Skip to documentation

Capability clusters

C2 · Authoring & knowledge UX

This cluster is the entire interactive surface of eli: a three-tier markdown editor (BlockNote block editor by default, raw textarea fallback, server-side env-flag switch), the vault navigation chrome (collapsible sidebar with buckets/types/folders/tags/favorites, breadcrumb header, cmdk command palette), the note metadata inspector (frontmatter properties, backlinks, version history, pin/star/archive/trash actions), a kanban projects board, daily notes, and the cross-cutting UX systems: a rebindable keybinding registry, FOUC-free theming, mobile adaptations, and keyboard accessibility. Every persistence path funnels through Next.js Server Actions in app/_actions/ against the storage abstraction (getStorage()), so the same components serve both Local (SQLite index + .md vault files) and SaaS (Supabase) modes — the SaaS note page calls this the ADR-0004 proof point (app/(saas)/(app)/w/[workspace]/n/[...path]/page.tsx:25-30).

Modes: Local + SaaS (shared editor/inspector/board; sidebar, palette, daily note, keybinding overrides are local-only) · Key dirs: components/editor/, components/sidebar/, components/note-list/, components/inspector/, components/projects/, components/ui/, lib/keybindings/, app/_actions/, app/(local)/v/, app/(saas)/(app)/w/ · Depends on: C1 storage, C4 AI routes, C7 realtime · Feeds: C3 embeds, C6 versions/diff, C5 agent setup, C9 cron

Capabilities

CapabilityWhat it doesEntry pointsModes
BlockNote block editor (default)Notion-style editor on @blocknote/react + @blocknote/shadcn; hydrates markdown via tryParseMarkdownToBlocks (single-paragraph fallback on parse failure, components/editor/blocknote-editor.tsx:91-102), serialises with blocksToMarkdownLossy, 1500 ms debounced autosave, manual Save + Cmd/Ctrl+S, Revert re-parses last saved body; mounted ssr:false with skeleton (blocknote-editor-dynamic.tsx:28-43)/v/n/[...path], /w/[workspace]/n/[...path]both
Custom slash menu with AI groupslashMenu={false} replaced by a SuggestionMenuController on / merging BlockNote defaults with 3 AI items — Continue writing (last 8000 chars to /api/ai/draft tier:fast), Generate from prompt, Summarise (blocknote-editor.tsx:371-404); tolerant JSON-lines/SSE text-delta stream reader (:433-462)/ inside BlockNoteboth
Wikilink autocompleteSecond SuggestionMenuController on [[ queries searchNotesAction (top 8 FTS hits) and inserts literal [[Title]] text — link resolution happens at save/index time, not in the editor (blocknote-editor.tsx:405-417)[[ inside BlockNoteboth
Raw textarea editor (legacy/mobile)Monospace markdown textarea with the same save/OCC/AI-toolbar contract (components/editor/note-editor.tsx); selected when ELI_LEGACY_EDITOR=1 (everywhere) or ELI_MOBILE_EDITOR=raw (≤767 px via matchMedia, mobile-editor-switch.tsx:29); decision tree is server-only (note-editor-root.tsx:33-35)env flagsboth
Debounced autosave + optimistic concurrencyBoth editors POST FormData {id, bodyMd, message?, expectedVersionId?, workspaceId?} to saveNoteAction after 1500 ms idle (SAVE_DEBOUNCE_MS, note-editor.tsx:34, blocknote-editor.tsx:52); stale expectedVersionId parks the loser’s body as a version instead of rejecting; save revalidates paths and fire-and-forgets scheduleEmbed (app/_actions/notes.ts:76-105)saveNoteActionboth
Conflict banner + snapshot reviewsnapshotVersionId renders an amber role=status aria-live=polite banner; “Review snapshot” sets window.location.hash to #version-<id>, PropertiesPanel’s hashchange listener highlights the History row (conflict-banner.tsx:41-79, properties-panel.tsx:72-81,261-277)editor after conflicting saveboth
AI transform toolbar menuRewrite (tone hint), Shorten, Expand, Fix grammar, Translate (default ‘French’) on selection or whole body via /api/ai/transform (ai-transform-menu.tsx:64-104, ≥3 chars); Summarise toolbar button → /api/ai/summarize toast (note-editor.tsx:157-180)editor toolbarboth
Note inspector (properties panel)Right aside (hidden below lg): pin/star/archive/download/delete actions; Type/Status/Tags blur-commit via updateFrontmatterAction; System card for _-prefixed frontmatter; Info stats; Backlinks with context; History (top 8 of 12 versions, Compare links to diff route) (properties-panel.tsx)note pagesboth
Inbox re-classification toastMutations compute bucket before/after; leftInbox triggers one-shot “Moved out of Inbox” toast (notes.ts:148-154, properties-panel.tsx:98-100)inspector editsboth
Two-pane vault shell, URL-driven filtersServer component parses ?bucket/type/tag/folder/favorites/q into a NoteFilter (default = all except archived), storage.listNotes limit 200, renders NoteList beside content in section#main-content (two-pane-shell.tsx:35-66)/v + filter URLslocal
Note listTitle, 200-char first-non-heading-line preview, type badge, ≤3 tag badges, relative time, star, selected highlight (note-list.tsx:49-84); search input debounces 250 ms then router.replace rewrites ?q= for server-side FTS (note-list-search.tsx:26-36)middle pane of /v*local
App sidebarshadcn Sidebar variant=inset collapsible=icon: buckets Inbox/All/Archive with counts, Favorites, Views (Today, Starred), Types, Folders, Tags — each a filter URL; data loaded per-request force-dynamic (app-sidebar.tsx:80, app/(local)/v/layout.tsx:18-24)/v* layoutlocal
Vault headerSticky breadcrumbs parsed from /v/n/<path> (intermediate segments → ?folder= links, vault-header.tsx:30-50), palette trigger, Re-scan (refreshVaultAction → cache reset + scanVault, app/_actions/vault.ts:121-133), theme toggle/v* headerlocal
Command palette (cmdk)Cmd/Ctrl+K; live searchNotesAction per keystroke (empty query = 20 recent, notes.ts:16-29); groups for notes, new note, navigation, vault ops (re-scan, external-tools setup, export zip, trash), theme; navigation is deliberately hard (window.location.assign, command-palette.tsx:131-140)Cmd+K, header buttonlocal
New note dialog (quick capture)Title + optional path; title-only lands at inbox/<slug>.md via NFKD slugifier (new-note-dialog.tsx:42,67-76); createNoteAction then hard navigation to /v/n/<path>Cmd+Shift+N, sidebar/list +, paletteboth (navigates to /v/n/*)
Daily note (Today)/v/today server-redirects to storage.todayNote() which returns-or-creates daily/YYYY-MM-DD.md with {type: daily, created} (app/(local)/v/today/page.tsx:4-8, lib/storage/local/writes.ts:482-494)/v/todaylocal
Projects kanban boardtype:project notes (limit 500) grouped by status into backlog/todo/in-progress/done + free-form columns, synonyms normalised (project-board.tsx:50-58); native HTML5 DnD, optimistic move with revert-on-error; setProjectStatusAction validates type and merges frontmatter (app/_actions/projects.ts:29-62)/v/projects, /w/[ws]/projectsboth
Keybinding registry + provider5 actions with defaults (lib/keybindings/registry.ts:39-49); one window keydown listener; per-action handler stacks, last-registered wins, return false chains down (keybinding-provider.tsx:84-100); single-combo only, no chordsglobal keydown, useKeybindingboth
Keybinding overrides + recorderLocal root layout merges ~/.eli/config.json#keybindings over defaults (app/layout.tsx:59-67); /keybindings page with capture-phase recorder (bare Escape cancels, keybinding-recorder.tsx:63-91); persisted via server actions + revalidatePath('/','layout'); no uniqueness validation/keybindingslocal
Shortcuts help overlayShift+? global dialog listing every action with override-aware combo, mac vs Ctrl detected post-mount (keybindings-help-dialog.tsx:44-46)? anywhereboth
ThemingInline pre-hydration script reads localStorage eli-theme and stamps class + colorScheme on <html> before paint (theme-script.tsx:18-32); provider follows OS matchMedia for ‘system’ and syncs tabs via the storage event (theme-provider.tsx:70-78)header toggle, paletteboth
Resizable editor/inspector split≥1024 px: react-resizable-panels with layout persisted to localStorage id eli-editor-inspector (editor default 75%/min 40%, inspector min 15%, resizable-shell.tsx:57-58,117-133); SSR/first paint renders editor-only until mounted since both branches touch window (:79-80); below lg, inspector becomes a bottom Drawer behind a floating button (:156-178)note pagesboth
Mobile adaptations (P6-MOBILE)SaaS hamburger Sheet <640 px with 44 px rows (components/saas/mobile-nav.tsx:35-72); toolbar buttons get [&>button]:max-sm:min-h-11; ELI_MOBILE_EDITOR=raw swaps editors ≤767 px; shadcn sidebar becomes offcanvas Sheet <768 px (hooks/use-mobile.ts:3); PWA metadata + PwaRegister (registers /sw.js only when NODE_ENV==='production', pwa-register.tsx:15; app/layout.tsx:26-51,94)viewport <640/768/1024 pxboth
Accessibility skip-linkFirst-tab “Skip to content” anchor, sr-only until focused, targets #main-content (app/layout.tsx:82-87, two-pane-shell.tsx:87, SaaS layout main); aria-labels on icon buttons, aria-live bannerTab from loadboth
Live vault refresh (SSE)VaultEventsBridge subscribes to /api/events (EventSource) and debounces router.refresh() 500 ms so external FS edits/agent writes/clipper drops appear live (vault-events-bridge.tsx:26-48); disable via ELI_DISABLE_LIVE_EVENTS=1 or window.__ELI_DISABLE_EVENTS__(local)/v/layout.tsxlocal
SaaS realtime co-edit toastRealtimeNoteWatcher on Supabase postgres_changes UPDATE filtered by note id; remote version change → non-destructive “changed elsewhere” toast with Refresh; own saves suppressed via markVersionAsLocal globalThis-pinned Set (max 32) (realtime/note-watcher.tsx:49-56,92-108)SaaS note pagesaas
External AI tools setup dialogMulti-select for Claude Code/Cursor/Codex CLI/Gemini CLI/OpenClaw; setupExternalToolsAction merges an eli MCP stdio entry pointing at bin/eli-mcp.mjs into each agent’s config; idempotent, per-agent result rows (components/setup/external-tools-dialog.tsx:43-51,89-99)palette commandlocal
Vault picker / sample vault/ redirects to /v when a vault is active; otherwise dialog registers a ~-expanded path in ~/.eli/config.json + immediate scan with counts (vault-picker.tsx:62-108, app/_actions/vault.ts:30-67); sample vault seeds ~/eli-sample with 3 wikilinked notes, written with the wx flag so existing files are never clobbered (vault.ts:69-108)/ (local)local
Note lifecycle server actionscreateNoteAction, saveNoteAction (OCC + embed), archive/unarchive, deleteNoteAction (soft-trash default, {hard:true} permanent, cancels pending embed, notes.ts:225-245), restoreNoteAction (SaaS clears trashed_at under RLS), emptyTrashAction({olderThanDays?}) shared with cron, pin/favorite toggles, updateFrontmatterAction, searchNotesActionapp/_actions/notes.tsboth
shadcn/ui primitive inventory55 vendored primitives in components/ui/ incl. command (cmdk), sidebar (cookie-persisted, built-in Mod+B toggle), resizable (react-resizable-panels), drawer/sheet, sonner, kbdcomponents/ui/*both

System view — simplified

System view — simplified

C2 is the app users actually touch: a markdown editor, a filterable note list with sidebar navigation, an inspector for metadata, and a kanban board — all client chrome around a thin server-action layer. Standalone (with AI, SaaS, and agents turned off) it is a complete offline Obsidian-style workspace: browse a vault by bucket/type/folder/tag, edit notes with versioned autosave and conflict snapshots, capture quickly with Cmd+Shift+N, keep a daily note, and drag projects across a board. Its only hard dependency is the C1 storage interface; AI menu items degrade to error toasts if /api/ai/* is unreachable. Every filter, search, and list state lives in the URL, so the server re-renders the tree — there is no client-side state store for navigation.

System view — detailed

System view — detailed
ComponentRoleFiles
Editor stackServer-side variant switch, BlockNote with slash-menu AI + wikilink autocomplete, raw textarea fallback, SSR-safe mount, mobile switch, conflict banner, AI transform menucomponents/editor/note-editor-root.tsx, blocknote-editor.tsx, blocknote-editor-dynamic.tsx, note-editor.tsx, mobile-editor-switch.tsx, conflict-banner.tsx, ai-transform-menu.tsx
Note actions layerServer actions for all note mutations + search: save (OCC, embed scheduling), create, archive, trash/restore/empty, pin, favorite, frontmatter patch, inbox-exit detection, project statusapp/_actions/notes.ts, favorites.ts, projects.ts, vault.ts, keybindings.ts
InspectorPin/star/archive/export/delete actions, blur-commit Type/Status/Tags, system-field display, info stats, backlinks, version history with diff links + conflict-snapshot highlightcomponents/inspector/properties-panel.tsx, backlinks-panel.tsx
Navigation shell (local)Sidebar filter tree, breadcrumb header with re-scan, two-pane filtered list layout, live SSE refresh bridgecomponents/sidebar/app-sidebar.tsx, vault-header.tsx, new-note-sidebar-button.tsx, components/two-pane-shell.tsx, components/note-list/*, components/vault-events-bridge.tsx, hooks/use-vault-events.ts, app/(local)/v/layout.tsx, app/(local)/layout.tsx
Command palettecmdk dialog: live FTS search, quick actions, navigation, theme switchingcomponents/command-palette.tsx, new-note-dialog.tsx, components/ui/command.tsx
Keybinding systemAction registry with defaults, combo matcher, config-file overrides merge, global provider with handler stacks, help overlay, rebind recorder + settings pagelib/keybindings/registry.ts, overrides.ts, components/keybinding-provider.tsx, keybindings-help-dialog.tsx, keybindings-table.tsx, keybinding-recorder.tsx, app/(local)/keybindings/page.tsx
ThemingFOUC-free boot script, light/dark/system provider with OS-follow and cross-tab sync, toggle dropdown; design-token stylesheet (class-based dark variant, oklch palette, radius scale) + cn()components/theme-script.tsx, theme-provider.tsx, theme-toggle.tsx, lib/theme/constants.ts, app/globals.css, lib/utils.ts
Note pagesRoute-level composition of editor + inspector + comments; local adds TwoPaneShell/favorites/today, SaaS adds workspace resolution, realtime watcher, share dialogapp/(local)/v/n/[...path]/page.tsx, app/(local)/v/page.tsx, app/(local)/v/today/page.tsx, app/(saas)/(app)/w/[workspace]/n/[...path]/page.tsx, components/resizable-shell.tsx, components/realtime/note-watcher.tsx
Projects boardKanban of type:project notes grouped by status, native DnD with optimistic moves, shared across modescomponents/projects/project-board.tsx, app/(local)/v/projects/page.tsx, app/(saas)/(app)/w/[workspace]/projects/page.tsx
Setup & onboardingVault picker + sample vault seeding; external AI agent MCP-config writer dialogcomponents/vault-picker.tsx, components/setup/external-tools-dialog.tsx, app/(local)/page.tsx
Mobile & a11y chromeSaaS hamburger sheet nav, mobile breakpoint hook, skip-link + PWA registration in root layoutcomponents/saas/mobile-nav.tsx, hooks/use-mobile.ts, app/layout.tsx, app/(saas)/(app)/layout.tsx
UI primitives55 vendored shadcn-style components (cmdk command, cookie-persisted sidebar with Mod+B, resizable panels, drawer/sheet, sonner, kbd)components/ui/

Data flow — simplified

Data flow — simplified

The autosave loop is the load-bearing runtime path of the whole platform: markdown comes out of storage, becomes BlockNote blocks, and every keystroke re-serialises it back to markdown, restarting a 1500 ms debounce timer. When the timer fires, saveNoteAction writes through the storage abstraction with an optimistic-concurrency token; a raced write is never rejected — the previous body is parked as a conflict-snapshot version and the editor shows a review banner. The save also revalidates the RSC tree and schedules a semantic re-embed without blocking the user.

Data flow — detailed

Open-and-edit-note autosave loop, including the markdown round-trip, both debounce values, the OCC divergence, and the local-vs-SaaS post-save divergence:

Data flow — detailed: the open-and-edit autosave loop

Quick capture → hard navigation (why the app deliberately abandons soft routing here):

Data flow — detailed: quick capture → hard navigation

Works separately / works together

Standalone

With every other cluster disabled except C1 storage, C2 is a complete offline markdown workspace: vault picker and sample-vault onboarding, sidebar navigation over buckets/types/folders/tags, URL-driven filtering and 250 ms-debounced search, the full BlockNote editing loop with versioned autosave and conflict snapshots, the inspector, quick capture, daily notes, the projects kanban, rebindable keybindings, and FOUC-free theming. AI features degrade gracefully (fetch failures surface as toasts); SaaS-only chrome (realtime watcher, share dialog, hamburger nav) simply never mounts.

Composed

  • C1 storage: every UI mutation goes through getStorage() (lib/storage) so LocalAdapter and SupabaseAdapter serve identical components — ADR-0004, called out at app/(saas)/(app)/w/[workspace]/n/[...path]/page.tsx:25-30. The /api/events SSE feed (FS watcher via lib/watch/events-bus) drives VaultEventsBridge refreshes.
  • C3 semantic search: saveNoteAction fire-and-forgets scheduleEmbed from lib/ai/auto-embed-queue (app/_actions/notes.ts:96-105); deleteNoteAction cancels pending embeds via cancelEmbed (notes.ts:225-245).
  • C4 AI platform: slash-menu items POST /api/ai/draft and /api/ai/summarize; the transform menu POSTs /api/ai/transform (blocknote-editor.tsx:371-404, ai-transform-menu.tsx:64-104).
  • C6 versioning: the History card links Compare to /v/diff/<path>?a&b (local) or /w/<slug>/diff (SaaS, parameterised diffRouteBase, properties-panel.tsx:56); conflict snapshots created by the storage adapter surface in this card via the hash deep-link.
  • C7 SaaS collaboration: SaaS note page adds RealtimeNoteWatcher (Supabase postgres_changes) and ShareLinkDialog (mounted when the editor toolbar receives workspaceId, note-editor.tsx:27-31); both note pages mount CommentsThread with section anchors from lib/storage/helpers/section-diff.
  • C5 agent surface: SetupExternalToolsDialog writes MCP configs (~/.claude/mcp.json, ~/.cursor/mcp.json, ~/.codex/config.toml, ~/.gemini/settings.json, ~/.openclaw/openclaw.json) pointing at bin/eli-mcp.mjs; storage.todayNote() is shared with the MCP daily tool (lib/mcp/tools/daily.ts:54).
  • C8 capture/templates: clipper drops and template-created notes appear live via the SSE bridge without a manual re-scan.
  • C9 platform ops: /api/cron/trash-prune reuses emptyTrashAction({olderThanDays}) (notes.ts:254-287); project status changes participate in git auto-commit via the standard updateNote path (app/_actions/projects.ts:12-16); e2e disables SSE via ELI_DISABLE_LIVE_EVENTS=1 / window.__ELI_DISABLE_EVENTS__.
  • → Export: inspector download hits /api/export/note/[id]; the palette’s “Export vault as zip” hits /api/export/vault.
  • → Config: keybinding overrides and the vault registry both live in ~/.eli/config.json via lib/config; the root layout re-reads it on every render in local mode (app/layout.tsx:59-67).

Load-bearing details

  • Editor selection is server-side and env-gated: ELI_LEGACY_EDITOR=1 forces the textarea everywhere, ELI_MOBILE_EDITOR=raw gives BlockNote-on-desktop/textarea-on-mobile, default is BlockNote everywhere (components/editor/note-editor-root.tsx:33-35; env.ts:56,72). Env vars never reach the client bundle.
  • The markdown round-trip is explicitly lossy by design: tryParseMarkdownToBlocks on load, blocksToMarkdownLossy on save; adversarial parse failures fall back to one paragraph block containing the raw markdown so no body is ever lost (blocknote-editor.tsx:91-102,135).
  • Saves are last-writer-wins-with-snapshot, not reject-on-conflict: a stale expectedVersionId still commits, and the previous head’s body is parked as an auto-conflict-snapshot version whose id rides back in SaveState.snapshotVersionId (app/_actions/notes.ts:35-47,76-89; conflict-banner.tsx:7-31).
  • Both debounce constants matter: autosave is 1500 ms (SAVE_DEBOUNCE_MS, note-editor.tsx:34, blocknote-editor.tsx:52), list search is 250 ms (note-list-search.tsx:9), SSE-driven refresh is 500 ms (vault-events-bridge.tsx:7).
  • Wikilink autocomplete inserts plain text [[Title]] — resolution and backlink extraction happen at save/index time in the storage layer, never in the editor (blocknote-editor.tsx:405-417).
  • Editor identity key is `${note.id}:${note.versionId}` so any external version change remounts the editor with fresh content (app/(local)/v/n/[...path]/page.tsx:102; SaaS page :152).
  • Palette and NewNoteDialog use window.location.assign instead of router.push because soft navigation races the layout revalidation triggered by revalidatePath('/v','layout') (command-palette.tsx:131-140; new-note-dialog.tsx:55-60).
  • Keybinding handler stacks give modal behaviour for free: last-registered handler wins per action, unmount pops the stack, returning false chains downward (components/keybinding-provider.tsx:63-100). The rebind recorder wins over the global provider by listening in the capture phase (keybinding-recorder.tsx:89). mergeKeybindingOverrides silently drops overrides for unknown action ids, so stale ~/.eli/config.json entries can never break the table (lib/keybindings/overrides.ts:19-34).
  • Frontmatter is partitioned by underscore prefix: _-prefixed keys are system fields (read-only System card, preserved on property edits, used for _pinned and _child_count); everything else is user-editable and fully replaced by updateFrontmatterAction (lib/storage/helpers/frontmatter.ts:92-102; notes.ts:354-360).
  • Trash is soft-delete by default; emptyTrashAction’s olderThanDays uses updatedAt as the trashed-at proxy and powers both the manual button and the daily cron (notes.ts:225-287). The inspector’s delete confirm reveals the local trash location .eli/trash/ (properties-panel.tsx:146-155).
  • Mobile tap-target compliance is a single Tailwind selector: [&>button]:max-sm:min-h-11 gives every toolbar button a 44 px minimum below 640 px (note-editor.tsx:216; blocknote-editor.tsx:318); MobileNav rows use h-11 (mobile-nav.tsx:64).
  • SaaS self-save suppression is globalThis.__ELI_LOCAL_VERSIONS__ — a Set of version ids capped at 32 with oldest-first eviction, stamped by markVersionAsLocal after each save; the realtime watcher never auto-replaces the buffer, only offers a Refresh toast (realtime/note-watcher.tsx:34-56,92-108).
  • Theme boots without FOUC via an inline <head> script that stamps the class before first paint from localStorage key eli-theme (lib/theme/constants.ts:12; theme-script.tsx:18-32); cross-tab sync rides the storage event (theme-provider.tsx:70-78). Browser chrome follows via the viewport themeColor pair #fafafa/#0a0a0a (app/layout.tsx:45-47).
  • Design tokens live in app/globals.css (Tailwind v4): dark mode is class-based, @custom-variant dark (&:is(.dark *)) — no prefers-color-scheme media query in CSS, so the ThemeScript/provider .dark class is the single switch (globals.css:5). @theme inline maps shadcn/sidebar/chart oklch variables to utilities; the palette is zero-chroma grayscale except --destructive and dark-mode --sidebar-primary (blue oklch(0.488 0.243 264.376)), and the entire radius scale derives from one --radius: 0.625rem token (sm 0.6x → 4xl 2.6x). cn() in lib/utils.ts is exactly twMerge(clsx(...)).
  • Two cross-cutting guards sit in @layer base (globals.css:119-160): body{overflow-x:hidden} is the P6-MOBILE no-horizontal-scroll guard (wide content must opt into its own overflow-x-auto), and a P6-A11Y focus-visible safety net draws a 2px var(--ring) outline on raw interactive elements — the :not([data-slot]) predicate excludes shadcn primitives that already ship their own focus ring, avoiding double indicators.

Gaps and partially wired

  • toggle-sidebar (Mod+\) is registered but dead: it appears in the registry, help overlay, and rebind table, but no component ever mounts a handler for it — the working sidebar shortcut is the shadcn built-in Mod+B ad-hoc listener (lib/keybindings/registry.ts:43; components/ui/sidebar.tsx:33,97-110). Verified: toggle-sidebar occurs nowhere outside registry/help/table.
  • No chord support: matchAction is single-combo only; the palette’s g t next to “Go to today” is a display-only CommandShortcut label (command-palette.tsx:187; lib/keybindings/registry.ts:70-84).
  • BlockNote ignores dark mode: BlockNoteView is pinned to theme="light" regardless of app theme (blocknote-editor.tsx:364); the textarea editor inherits app theming correctly.
  • SaaS backlink links are wrong-mode: PropertiesPanel backlink hrefs are hardcoded to the local /v/n/ prefix even when rendered inside /w/<slug>/n/..., so SaaS backlink clicks navigate to a local-mode route (properties-panel.tsx:241); contrast diffRouteBase which IS parameterised (:56).
  • SaaS favorites stubbed: the SaaS note page hardcodes favorite state to false pending a user_favorites table (app/(saas)/(app)/w/[workspace]/n/[...path]/page.tsx:88-89,135).
  • Keybinding overrides are local-only: the SaaS root layout deliberately skips the ~/.eli/config.json read (“per-user keybindings in saas would be a future profiles-row column”, app/layout.tsx:54-67); /keybindings returns notFound in SaaS (app/(local)/keybindings/page.tsx:21).
  • No rebind uniqueness validation: assigning the same combo to two actions is silently resolved by handler precedence, not flagged as an error.
  • No SaaS daily note: /v/today is local-only; no /w/<slug>/today route exists.
  • Daily-note day boundary is UTC: todayNote() computes the date as new Date().toISOString().slice(0,10) (lib/storage/local/writes.ts:483), so “Today” rolls over at UTC midnight — an evening user in the Americas gets tomorrow’s note.
  • No SaaS command palette: the palette mounts in the local vault header; SaaS reaches notes via top-bar links instead.
  • Trash age proxy: emptyTrashAction({olderThanDays}) measures against updatedAt rather than a real trashed-at timestamp (notes.ts:254-287), so a note edited just before trashing ages faster than expected.
  • Undebounced hook exposed: a public useVaultEvents hook exists alongside the debounced bridge (hooks/use-vault-events.ts:17-40) but nothing else consumes it yet.

See also