Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

API Package (@lapis-notes/api)

The API package is the foundational library that every other package depends on. It defines the runtime services, data structures, view hierarchy, plugin contract, and event system that together form the application kernel.

Source: packages/api/src/lib/

It also owns the shared internal-link formatting helpers that metadata and file-management surfaces use to serialize Obsidian-style vault links from a target path plus source note context, and FileManager routes rename-time backlink rewrites through those same helpers so link updates follow the configured file-link formatting rules for markdown notes and other vault files. MetadataCache resolves those short link paths back to markdown notes and other vault files, including bare filenames and unique suffix paths for embeds.

FileManager.processFrontMatter() also preserves unrelated keys when callers mutate frontmatter before metadata indexing has populated a cache entry by seeding writes from parsed file content instead of rewriting from an empty cache object.

The package also maintains a curated docs-only export surface at packages/api/src/lib/docs-api.ts for the public lapis.md TypeScript API reference. Public TSDoc on that surface is the source of truth for generated summaries, parameter descriptions, and return descriptions published under /developers/api-reference/.

App — Central Service Container

The App class (in context.svelte.ts) owns every runtime service. It is the single root object that plugins, views, and the workspace shell receive.

Plugin Distribution Contracts

The API package exposes plugin-distribution primitives for the Full Registry V1 installer path. This module is the shared contract surface for registry metadata, .lapis-plugin bundles, signed release manifests, installed provenance state, canonical JSON, SHA-256 verification, Ed25519 signature verification, DEFLATE-capable bundle extraction, compatibility checks, safe plugin release paths, and reserved plugin-id policy.

The distribution layer treats official status as verified external provenance, not as plugin-controlled manifest content. Manifests loaded from .obsidian/plugins/<plugin-id>/manifest.json therefore remain community-style runtime inputs unless a verified installer records official provenance in .obsidian/installed-plugins.json.

The official registry client remains separate from PluginManager. It fetches signed index and detail metadata from a locked registry source, verifies inline Ed25519 signatures with embedded trusted keys, normalizes relative metadata URLs against the registry index URL, rejects reserved-id policy violations, and can serve a stale adapter-backed cache from .obsidian/plugin-registry-cache.json when network refresh fails. It also fetches and verifies the signed revoked.json policy document for official sources and caches revocation state beside catalog metadata.

Verified installation is also owned by the distribution module rather than the runtime plugin manager. The installer downloads one official .lapis-plugin bundle, verifies the catalog bundle SHA-256 and size, extracts release.signed.json, verifies the signed release manifest, checks every embedded file’s path, size, and SHA-256 hash, rejects unsigned extra bundled files and manifest ID/version mismatches, stages files under .obsidian/plugins/.installing/, swaps them into the Obsidian-compatible plugin folder, and records provenance in .obsidian/installed-plugins.json. Manual installBundle() uses the same verified official bundle path for a local .lapis-plugin file. Bundle entries may be stored or DEFLATE-compressed; the signed release manifest remains stored as the first archive entry. The default distribution manager selects bundleVerification: "auto" for web installs, which verifies bundle hash/size, signature, and signed files in a module worker when available and falls back to the main-thread verifier. Tests and non-web hosts can force bundleVerification: "main-thread". Progress events include verifying-bundle before signed-file verification, and extraction, verification, and staging report processed byte/file progress when available. Fresh registry installs and manual .lapis-plugin installs enable the plugin by default when a plugin manager is present; update paths pass the previous enabled state explicitly so disabled plugins stay disabled. Uninstall removes plugin code and the enabled community-plugin entry while preserving data.json by default. Install and update operations accept a cooperative AbortSignal and emit phase-based progress events. Bundle downloads report aggregate byte progress when the response stream or fallback body size is available, so callers can surface determinate status-bar progress without weakening signature, size, or hash verification.

The distribution layer also validates signed runtime metadata for official ESM-only external releases before staging. It compares signed release runtime metadata with packaged manifest.json Lapis entries, rejects official CommonJS entries and fallbackPath, checks module-format file extensions, scans entry sources for bare imports, and validates declared or scanned shared dependencies against the generated host-module catalogue. Official failures are hard install errors; manual and community compatibility paths can retain CommonJS/fallback metadata and surface the same diagnostics as warnings on installed provenance records.

PluginDistributionManager applies verified revocation policy to installed official records, lists compatible updates, surfaces incompatible latest releases with compatibility reasons, and treats revoked installed versions as a distinct state that can be disabled or updated to a compatible replacement. Verified updates reuse the same staged installer path as fresh installs and preserve the plugin’s enabled state when the runtime reports it as enabled. Update progress and cancellation use the same installer signal and progress event contract as fresh installs.

The package also owns the loader-side contracts used by the structured ESM/CommonJS transition. lapis-extension.ts accepts structured lapis.runtime.entries metadata beside the legacy runtime strings, plugin-runtime-entry.ts contains the shared selector for workspace, Electron renderer, and Electron sidecar entry choice, plugin-dependency-resolver.ts defines the shared dependency resolver seam, and plugin-dependency-scanner.ts provides conservative runtime diagnostics for bare require, import, dynamic import(), and re-export specifiers. Generated host-module registry metadata under src/lib/generated/ is consumed by diagnostics and distribution validation so the app does not maintain a second dependency allowlist.

plugin-asset-server.ts defines the plugin module URL shapes used by renderer ESM loading, including hash-bearing /__lapis/plugins/... routes for web/PWA and lapis-plugin://... URLs for Electron renderer, plus path normalization, MIME type selection, and installed-file size/hash verification helpers. The package does not serve those URLs itself; host-specific asset serving remains owned by the web and Electron packages. When a host provides PluginAssetServer, PluginManager uses a hybrid execution host that imports structured ESM entries from verified module URLs, keeps CommonJS and sidecar entries on their existing hosts, and falls back to CommonJS only when the ESM descriptor declares fallbackPath. Community plugin diagnostics include the selected host and module format, fallback entry and fallback-used status, reload-on-update policy, asset URL mode and URL, declared/scanned shared dependencies, undeclared and missing shared dependencies, plus deprecated and private host-module usage.

Properties

PropertyTypePurpose
i18nLocalizationManagerInternationalization
sessionVaultSession | undefinedRuntime/vault/database session resolved by the shell
workspaceWorkspaceLayout, leaves, view registry
vaultVaultFile system access
appDatabaseAppDatabasePer-vault generated metadata/search/app state
pluginsPluginManagerPlugin lifecycle
pluginDistributionPluginDistributionManagerVerified plugin registry, install, update, uninstall, and provenance state
internalPluginscompatibility facadeEnabled bundled-plugin lookup for Obsidian-style plugin compatibility
scopeScopeRoot keyboard scope
keymapKeymapScope stack manager
settingsAppSettingsSettings groups container
editorsMap<string, Editor>Open editors by ID
editorSuggestMap<string, EditorSuggestManager>Autocomplete managers
configurationConfigurationSchemaJSON-schema config system
fileManagerFileManagerFile operation helpers

Browser vault sessions now resolve coordinated SQLite ownership before App construction, but defer the actual appDatabase.open() work until the mounted workspace shell begins background metadata hydration so the coordinated database boundary retains SQLite-to-fallback handling without blocking first paint. Desktop sessions use the same VaultSession boundary to resolve a native folder adapter plus a native app database bridge, keeping generated state in host-owned native SQLite storage outside the selected vault while preserving the same AppDatabase contract that browser runtimes use. Saved VaultProfile records may also carry optional demo metadata (source, fixtureVersion) so renderer flows such as the bundled demo workspace can remain identifiable across chooser restores and reset commands; browser OPFS recreation preserves that demo metadata when reopening an existing saved profile. The neutral native desktop bridge also carries host-advertised platform details and a service capability registry for resource, database, search, notebook, model, plugin-sidecar, file-watch, notifications, and file-system action providers. The vault contract exposes async resource URLs with explicit revocation so browser adapters can return blob URLs and native adapters can return host-owned URLs for read-only media surfaces. | markdownPostProcessor | MarkdownPostProcessor[] | Sorted post-processors | | markdownCodeBlockPostProcessor | Record<string, Function> | Language-keyed code processors | | metadataTypeManager | MetadataTypeManager | Frontmatter type tracking | | commands | CommandManager | Command registry and palette | | contextKeys | ContextKeyService | Declarative context-key store and when evaluator | | statusBar | StatusBarManager | Declarative status bar contribution registry with stable-order in-place updates | | urls | AppUrlService | Lapis/Obsidian-style app URL parser, opener, and plugin protocol-handler registry | | metadataCache | MetadataCache | File metadata index | | notifications | NotificationManager | In-app notification records and progress handles | | embedRegistry | EmbedRegistry | Embed view handlers | | lastEvent | Event | Last UI event | | renderContext | MarkdownFileInfo | Markdown render context | | secretStorage | SecretStorage | Encrypted key-value store | | languageServices | LanguageServiceManager | Host-neutral diagnostics, completion, hover, definition, and code-action providers |

Key Methods

StatusBarManager owns the API-side registry used by the workspace shell. Items can be registered declaratively, gated by when expressions against App.contextKeys, and updated in place through upsertItem() without losing their original registration order. That lets first-party contributors such as notifications change text, tooltip, command, icon, and visibility as background state changes while Plugin.addStatusBarItem() remains the raw-DOM compatibility escape hatch.

AppUrlService owns app URL parsing and dispatch. It accepts lapis://open?vault=...&file=..., the existing lapis-notes:// alias, installed-PWA web+lapis:// launches routed through the web host, Obsidian-style vault and absolute-path shorthands, and plugin-registered custom actions. The built-in open action validates that vault-targeted URLs refer to the current vault by id or name, resolves markdown paths with optional .md omission, opens non-markdown files through the normal workspace file-view routing, and keeps custom action handlers reversible through plugin unload.

  • openFile(file, opts?) — Opens a file in the workspace by creating or reusing a main-area leaf.
  • registerEditorExtension(ext, viewType?) — Adds a CodeMirror extension globally or scoped to a view type.
  • registerMarkdownPostProcessor(processor, sortOrder?) — Inserts a post-processor in sorted order.
  • registerMarkdownCodeBlockProcessor(language, handler, sortOrder?) — Registers a fenced-code-block renderer.

Notifications And Progress

App owns a host-neutral NotificationManager at app.notifications. The manager is the single runtime API for in-app messages, persisted notification history, and long-running progress. The required bundled notifications plugin owns the visible renderer surfaces; callers do not talk to Svelte, svelte-sonner, browser notification APIs, or desktop host APIs directly.

Public operations:

  • notify(options) records an information, warning, or error notification and optionally persists it.
  • createProgress(options) creates a progress handle for long-running work.
  • withProgress(options, task) creates a progress handle, passes a cooperative cancellation signal/token to the task, and completes or fails the handle when the task settles.
  • list(), markRead(id), clear(id), and clearAll() manage the current notification history.

Progress reporting accepts both determinate counters (current / total) and VS Code-style incremental reports (increment, where increments accumulate toward 100). If neither form is available the progress item is indeterminate. Progress handles carry title, source, message, location (status, notification, or silent), cancellability, terminal state, and an AbortSignal for cooperative cancellation. The manager never preempts synchronous plugin code; cancellation is delivered to work that checks the signal between natural async boundaries.

Notification history is generated per-vault app state. Durable notifications are persisted through AppDatabase, while live progress is session state unless the caller asks to persist terminal success or failure. Desktop hosts may mirror selected durable records to native OS notification APIs through the neutral native bridge; the shared app.notifications API remains renderer- and host-neutral.

Context Injection

The App instance is provided to Svelte components via setApplicationState(app) / useApplicationState() using Svelte context, avoiding prop-drilling.


Workspace — Layout and View Management

The Workspace class (in workspace.svelte.ts) manages the split-pane layout, tab groups, leaves, and the view type registry.

Layout Tree

Workspace
├── rootSplit: WorkspaceSplit   (center)
├── leftSplit: WorkspaceSplit   (left sidebar)
└── rightSplit: WorkspaceSplit  (right sidebar)
    └── children: (WorkspaceSplit | WorkspaceTabs)[]
        └── WorkspaceTabs
            └── children: (WorkspaceLeaf | WorkspaceSidebarGroup)[]
                ├── WorkspaceLeaf
                │   └── view: View
                └── WorkspaceSidebarGroup
                    └── children: WorkspaceLeaf[]
                        └── view: View

WorkspaceSplit

Represents a vertical or horizontal partition:

  • children: (WorkspaceSplit | WorkspaceTabs)[] — Nested splits or tab groups.
  • sizes: number[] — Proportional sizes for resizable panes.
  • type: "vertical" | "horizontal" — Split direction.
  • parent — Parent split reference.

WorkspaceTabs

A group of tabbed workspace children:

  • children: (WorkspaceLeaf | WorkspaceSidebarGroup)[] — Leaves in normal tab groups, and sidebar groups where the tab group belongs to a sidedock.
  • currentTab: number — Active tab index (reactive via $state).
  • stacked: boolean — Stacked vs. tabbed display mode.
  • addLeaf(leaf), removeLeaf(leaf) — Manage children.

WorkspaceSidebarGroup

A sidebar-only container that behaves like a VS Code-style view container while keeping each child as a real WorkspaceLeaf:

  • id, name, icon — Stable group identity and display metadata.
  • children: WorkspaceLeaf[] — Ordered child leaves mounted as collapsible panels by the workspace shell.
  • hiddenLeafIds — Persisted child visibility state; hidden leaves remain in the group.
  • collapsed — Persisted per-child collapsed panel state.
  • panelSizes — Optional persisted per-child panel proportions used by the workspace shell when grouped sidebar panels are resized.

WorkspaceLeaf

A single pane that hosts a view:

  • view: View | null — The currently mounted view.
  • containerEl: HTMLElement — DOM container.
  • id: string — Unique identifier.
  • openFile(file, opts?) — Opens a file using the view type registry. When no registered handler exists, it may mount the plugin-install-prompt fallback for verified official registry matches.
  • setViewState(state) — Switches or configures the view.
  • getViewState() — Serializes view state for layout persistence.

WorkspaceRibbon

Left-side icon strip. addRibbonIcon(icon, title, callback) adds items.

WorkspaceSidedock

Wraps a WorkspaceSplit with show/hide/collapse semantics for left and right sidebars.

View Type Registry

workspace.registerView(type: string, creator: (leaf) => View)
workspace.registerEditorView(contribution)
workspace.registerSidebarView(type: string, options)
workspace.registerExtensions(exts: string[], viewType: string)

Extensions (e.g., .md or .notebook.md) map to view types. registerEditorView() registers stable editor-view metadata used by settings and association resolution; registerExtensions() also backfills compatibility metadata for older registrations. When a file is opened, the workspace first checks workspace.editorAssociations for matching VS Code-style glob patterns, then falls back to registered editor-view filename patterns, and finally falls back to the longest matching registered suffix. The resolved view type is looked up in the constructor registry and mounted in the leaf.

registerSidebarView(type, options) records default sidebar placement metadata for non-file views. ensureSideLeaf(type, options) can use those defaults, or explicit options such as side, group, groupTitle, groupIcon, and hidden, to create the leaf in a normal sidebar tab or under a WorkspaceSidebarGroup. Plugin.registerSidebarView(type, creator, options) wraps view registration and placement cleanup for plugin-owned sidebar contributions.

Default file navigation targets the root split. workspace.getLeaf(false), workspace.openLinkText(...), and app.openFile(file) reuse the active main-area leaf or selected root tab, and create a root leaf when none exists. They do not reuse an active sidebar leaf; sidebar views are opened through explicit sidebar APIs such as ensureSideLeaf(), getLeftLeaf(), and getRightLeaf().

Layout Persistence

getLayout() → JSON, loadLayout(json), requestSaveLayout() (debounced). The JSON captures the full tree: splits, tabs, leaves, sidebar groups, hidden/collapsed/resized group panel state, and view states. Old flat sidebar tab JSON still loads without group entries, and grouped sidebar JSON without panelSizes falls back to equal panel proportions.

Key Workspace Methods

MethodPurpose
loadLayout(json)Restores layout from persisted JSON
changeLayout(layout)Applies a new layout tree
getLayout() / toJson()Serializes current layout
revealLeaf(leaf)Activates and scrolls to a leaf
registerView(type, creator)Registers a view type constructor
registerEditorView(contribution)Registers stable file-editor metadata for settings and association routing
registerSidebarView(type, options)Registers default sidedock placement for a view type
registerExtensions(exts, viewType)Maps file suffixes to view types
getLeaf(newLeaf?)Returns the active floating/popout leaf or root leaf for default file navigation, or creates explicit panes
determineViewType(ext)Resolves an extension or view type alias → view type
iterateRootLeaves(callback)Visits every leaf in the root split
updateOptions()Re-applies editor extensions to all editors
requestSaveLayout()Debounced layout persistence
moveWorkspaceChildToFloating()Moves a leaf/sidebar item into an in-app floating pane
moveWorkspaceChildToPopout()Moves a leaf/sidebar item into a host-backed popout window when supported
  • registerHoverLinkSource(id, info) stores hover-preview source registrations for plugin compatibility and supports unload cleanup through the plugin lifecycle helpers.
  • openPopoutLeaf() and moveLeafToPopout() now route through a host popout capability. Browser and Electron hosts open a separate Window/Document that still renders the shared WorkspaceWindow tree, while unsupported hosts raise an explicit error without detaching the source leaf. Drag-out layout gestures still use the separate in-app floating-pane path.
  • Floating WorkspaceWindow entries track normal, collapsed, minimized, and maximized display state. Collapsed and minimized state serialize with the live workspace layout; maximized state is renderer-only and restores as normal after reload.

View Hierarchy

Defined in view.svelte.ts. All views extend the Component base class for lifecycle management.

Component
└── View
    └── ItemView
        └── FileView
            ├── TextFileView
            │   └── MarkdownView
            └── EditableFileView

Component (base)

  • children: Component[] — Child lifecycle management.
  • containerEl: HTMLElement — Root DOM element.
  • loaded: boolean — Lifecycle state.
  • load() / unload() — Lifecycle hooks.
  • register(cleanup) — Registers teardown callbacks.
  • registerEvent(ref) — Auto-cleaned event subscriptions.
  • registerDomEvent(el, type, handler) — Auto-cleaned DOM listeners.
  • registerInterval(id) — Auto-cleared intervals.

View

Abstract base for all views:

  • icon: string — Lucide icon identifier.
  • leaf: WorkspaceLeaf — Owning leaf.
  • scope?: Scope — Keyboard scope (pushed when leaf is active).
  • abstract getViewType(): string
  • abstract getDisplayText(): string
  • getState() / setState(state) — Serialization for layout persistence.

ItemView

Adds a toolbar actions bar:

  • actions: Array<{icon, title, callback}> — Toolbar buttons.
  • contentEl: HTMLElement — Read-only content container below the toolbar.
  • addAction(icon, title, callback) — Registers a toolbar button.

FileView

Associates a view with a file:

  • file: TFile | null — The backing file.
  • abstract onLoadFile(file): Promise<void> — Called when a file is loaded.
  • abstract onUnloadFile(file): Promise<void> — Called when file is unloaded.
  • abstract onRename(file): Promise<void> — Called on file rename.
  • abstract canAcceptExtension(ext: string): boolean — Declares supported extensions.

TextFileView

Text editor base backed by CodeMirror 6:

  • editor: Editor — CodeMirror wrapper instance.
  • data: string — Current document content.
  • adapter: DataAdapter — Deprecated compatibility accessor for direct adapter access.
  • abstract getViewData(): string, setViewData(data, clear?), clear() — Subclass data hooks.
  • save(clear?) — Writes content through app.vault.modify().
  • onLoadFile(file) — Reads through app.vault.read(), then sets editor extensions.
  • onRename(file) — Updates file reference.

MarkdownView

Extends TextFileView with dual-mode rendering:

  • previewMode: MarkdownPreviewView — Preview renderer.
  • currentMode: MarkdownSubView — Source / preview / live-preview state.
  • hoverPopover: HoverPopover | null — Page hover preview.

EmptyView

Placeholder for new tabs. View type "empty". Mounts EmptyViewComponent with create/open/recent/close actions. Uses localized display text via t("base/empty-view", "New Tab"). When WorkspaceLeaf.setViewState() receives an unsupported view type, the workspace falls back to this view, preserves the original requested type in the view state under state.__missingViewType, and displays that identifier in the empty view title.


Plugin System

Plugin Base Class

Every plugin extends Plugin (which extends Component):

  • basePath: string — Plugin folder path.
  • manifest: PluginManifest — Parsed manifest.json.
  • enabled: boolean — Current state.
  • hostMode: string — Community plugin execution host used for diagnostics.
  • requestedCapabilities / grantedCapabilities — Runtime capability declarations and broker decisions surfaced for hosted plugin diagnostics.
  • lastFailureMessage / failureCount — In-memory diagnostic history across failed enable, disable, and restart attempts.
  • app: App — App instance.
  • abstract onload(): Promise<void> | void — Initialization hook.
  • onunload(): Promise<void> | void — Cleanup hook.
  • enable() / disable() — Toggle with "enable" / "disable" event emission.

Baseline Obsidian-style manifests remain valid. External manifests may additionally declare supportedRuntimes, requiredCapabilities, and executionHints; PluginManager treats missing declarations as compatible, but rejects explicit runtime or capability requirements before evaluation when the selected host cannot satisfy them.

External Plugin Diagnostics

PluginManager keeps external plugin diagnostics separately from loaded plugin instances so preflight and evaluation failures remain visible even when no Plugin object could be registered. Diagnostics include plugin ID/name, manifest summary metadata (version, author, and description), source (official or community), provenance, selected host mode, activation mode (code, manifest-only, or not-activated), the selected runtime entry when known, requested capabilities, granted capabilities, lifecycle/preflight state, and the last failure message. Indexed Lapis-extension metadata remains available alongside those diagnostics so the workspace UI can show permission declarations, granted app permissions, and trusted-desktop warnings without depending on a loaded Plugin instance. Runtime-entry preflight also validates that Lapis code entries stay within the plugin root and use supported JavaScript file extensions before evaluation is attempted. The manager exposes the current host ID, a full diagnostics list, per-plugin lookup, and restartPlugin(pluginId) so the workspace UI can retry failed plugins without first removing them from the external enablement configuration.

Registration API

Plugins register functionality during onload():

MethodPurpose
addCommand(cmd)Register command (auto-prefixed with plugin ID and name)
removeCommand(id)Unregister command
registerShortcut(handler)Register keyboard binding
registerView(type, creator)Register a view type constructor
registerSidebarView(type, creator, options)Register a view plus default sidedock placement
registerEditorExtension(ext, viewType?)CodeMirror extension (plain or factory)
registerMarkdownPostProcessor(proc, order?)HTML post-processing
registerMarkdownCodeBlockProcessor(lang, handler, order?)Fenced code blocks
registerEditorSuggest(suggest, viewType?)Autocomplete provider
registerExtensions(exts, viewType?)File extension → view mapping
registerTypeWidget(widget)Custom frontmatter editor widget
registerMetadataProcessor(processor, ext?)Custom metadata parser
registerHoverLinkSource(id, info)Hover preview source
registerObsidianProtocolHandler(action, handler)obsidian:// URI handler
registerCliHandler(name, prefix, callback)CLI commands
registerBasesView(viewId, registration)Contribute Bases views through the shared Bases registry
addRibbonIcon(icon, title, callback)Left ribbon button
addStatusBarItem()Status bar HTML element
addSettingTab(tab)Settings panel grouped by runtime source: bundled/official under “Core plugins”, community/manual under “Community plugins”
loadData() / saveData(data)Persistent plugin storage via canonical app.json pluginData, mirrored to legacy Obsidian plugin files

Suggest Helpers

AbstractInputSuggest, SuggestModal, and FuzzySuggestModal provide the shared Obsidian-style suggestion helpers used by plugins and editor-adjacent UI:

  • input/modal suggestion lists refresh from the current query string
  • arrow keys move the active item
  • Enter and Tab choose the active item
  • Escape closes the suggestion UI
  • instruction rows render when provided through setInstructions(...)

Plugin Manifest

interface PluginManifest {
  id: string;
  name: string;
  author: string;
  version: string; // semver
  minAppVersion: string; // semver
  description: string;
  dir?: string; // plugin folder in vault
  authorUrl?: string;
  isDesktopOnly?: boolean;
  supportedRuntimes?: string[];
  requiredCapabilities?: HostedPluginCapability[];
  executionHints?: Record<string, unknown>;
  lapis?: LapisExtensionManifest;
}

PluginManager

Owns the full plugin lifecycle. Extends EventDispatcher.

Properties:

  • plugins: Map<string, Plugin> — Loaded plugins keyed by ID.
  • lapisExtensions: LapisIndexedExtension[] — Manifest-indexed Lapis extensions keyed by discovery, including manifest-only extensions that did not instantiate plugin code.
  • dependencies: Record<string, any> — Module dependencies for require() injection.
  • pluginsPath: string"/.obsidian/plugins".
  • adapter: DataAdapter — File system.

Lifecycle:

  1. loadPlugins() — Ensures the plugin folder exists, reads installed provenance from .obsidian/installed-plugins.json, discovers external plugin manifests, reads community-plugins.json, then activates bundled plugins, configured official plugins, and configured community plugins before emitting "plugins-loaded".
  2. loadPlugin(path) — Loads a single external plugin. Parses manifest.json, resolves source/provenance from verified installed state or caller options, validates/classifies optional lapis metadata, indexes Lapis contributions before code evaluation, runs manifest preflight, and evaluates the selected runtime entry when an Obsidian-compatible or hybrid plugin needs code. Multi-file CommonJS plugin builds can use relative require() calls for local .js, .cjs, and .mjs chunks under the plugin folder. Structured ESM entries load through the host-provided plugin asset URL server when available. Manifest-only Lapis extensions may be indexed without a Plugin instance. Emits "plugin-loaded" only for instantiated plugins.
  3. enablePlugin(id) — For instantiated plugins, calls plugin.enable(), injects registered bundled core-plugin CSS or reads external styles.css if present, and appends a <style id="plugin-css-{id}"> element to <head>. For manifest-only Lapis extensions, installs declarative commands and configuration schemas without evaluating code and records diagnostics for indexed contribution kinds that are not installable yet. Saves enabled state to community-plugins.json for official/community external plugins or core-plugins.json for bundled/system plugins as appropriate. Emits "plugin-enabled" only for instantiated plugins.
  4. disablePlugin(id) — For instantiated plugins, calls plugin.disable() and removes the CSS element. For manifest-only Lapis extensions, disposes manager-installed command/configuration effects. Saves state. Emits "plugin-disabled" only for instantiated plugins.

Dynamic Import:

External plugin JavaScript evaluation runs through CommunityPluginExecutionHost. The default RendererCommunityPluginExecutionHost loads CommonJS chunks as strings, wraps each evaluated module in new Function('exports', 'module', 'require', ...), and executes it in the renderer. The injected renderer require() resolves relative local chunks from the same plugin folder and external modules from the registered dependency resolver. Unknown modules throw an error. When a host supplies PluginAssetServer, PluginManager installs a hybrid execution host: structured ESM entries are imported from version/hash plugin asset URLs, CommonJS entries continue through the CommonJS host, Electron sidecar entries stay delegated to the sidecar host, and ESM-to-CommonJS fallback is attempted only when the selected runtime entry declares fallbackPath. The manager records whether fallback was actually used, the asset URL mode and URL used for renderer ESM imports, and the selected runtime’s reload requirement so the settings UI can explain loader behavior after activation. The native sidecar bridge receives the same selected runtime metadata and wraps sidecar evaluation failures with host, format, and entry context while preserving the original dependency or local-graph diagnostic.

Hosted Plugin Capabilities:

HostedPluginCapabilityFacade defines the initial structured, serializable capability surface for sidecar-hosted community plugins: vault read/write, plugin data, commands, notices, settings reads/surfaces, metadata queries, approved event subscriptions, and logging. The facade forwards every call to a HostedPluginCapabilityBroker request instead of exposing the live renderer App graph, DOM objects, or Node.js primitives.

Events: plugins-loaded, plugin-loaded, plugin-enabled, plugin-disabled, plugin-error, css-change.

Current boundary: PluginManager is still the lifecycle owner. It performs manifest preflight, CSS loading, source/provenance classification, and failure reporting in the active renderer/runtime process. External plugin code evaluation is delegated to the configured execution host; the default renderer host still evaluates CommonJS modules in process.

Approved direction: ADR-001: Plugin Runtime Rework defines the runtime-neutral execution-host and extension-manifest direction. The current implementation now covers classification, Lapis namespace validation, contribution indexing, activation-event lazy loading, generated extension state, manifest-only command/configuration installation, scoped language-service provider registration, bundled or code-backed system-extension registration, and host-selected workspace or trusted-desktop code entry loading with per-plugin Electron-sidecar hardening. The remaining runtime work is limited to richer manifest-only coverage plus follow-on runtime expansion such as browserWorker and multi-host hybrid activation.


Storage — Vault, Session, and App Database

DataAdapter Interface

Compatibility contract for path-based file system operations:

interface DataAdapter {
  getName(): string;
  exists(path): Promise<boolean>;
  stat(path): Promise<Stat | null>; // {type, ctime, mtime, size?}
  read(path): Promise<string>;
  readBinary(path): Promise<ArrayBuffer>;
  write(path, data, opts?): Promise<void>;
  writeBinary(path, data, opts?): Promise<void>;
  append(path, data, opts?): Promise<void>;
  appendBinary(path, data, opts?): Promise<void>;
  process(path, fn: (data) => string, opts?): Promise<string>;
  list(path): Promise<{ files: string[]; folders: string[] }>;
  mkdir(path, opts?): Promise<void>;
  rmdir(path, recursive): Promise<void>;
  remove(path): Promise<void>;
  rename(path, newPath): Promise<void>;
  copy(path, newPath): Promise<void>;
  getResourcePath(path): string; // For <img src>
  trashSystem(path): Promise<boolean>; // OS trash
  trashLocal(path): Promise<void>; // .trash folder
}

VaultAdapter is the preferred storage contract for new runtime work. It extends DataAdapter today and can expose optional capabilities such as persistence, user-visible files, permission requirements, native watching, resource URLs, and system trash support. DataAdapter remains exported for Obsidian-style plugin compatibility and older call sites.

VaultSession

VaultSession groups the runtime-selected storage objects that App needs:

interface VaultSession {
  runtime: "web-pwa" | "electron-desktop" | "test";
  profile?: VaultProfile;
  vaultAdapter: VaultAdapter;
  appDatabase?: AppDatabase;
  appDatabaseState: {
    status: "ready" | "blocked";
    mode:
      | "sqlite-owner"
      | "sqlite-proxy"
      | "sqlite-blocked"
      | "indexeddb-fallback"
      | "memory-fallback";
    lockSupported: boolean;
    message?: string;
  };
  awaitAppDatabase?: (options?: {
    signal?: AbortSignal;
  }) => Promise<VaultSession>;
}

The browser workspace resolves this before constructing App. When a competing tab already owns the per-vault SQLite OPFS database, the preferred browser path now returns a ready session backed by a coordinated proxy AppDatabase that forwards requests to the owner tab over BroadcastChannel and can later promote itself to owner when the lock becomes available. The blocked state remains as a fallback only when delegation cannot be established. Adapter-only construction is still supported for compatibility and tests; in that path App creates a default app database from the adapter’s vault ID.

AppDatabase

AppDatabase stores per-vault generated state separately from canonical vault files. The contract covers schema migration, metadata snapshots, indexed file records, links, tags, properties, search document records, notebook output and scalar input snapshots with per-cell source fingerprints, explicit FTS rebuild support, cheap search-index stats, optional search-index batching for refresh-heavy backends, and snippet-rich search query access with raw lexical/vector scores plus explainable Reciprocal Rank Fusion breakdowns for hybrid retrieval.

MetadataCache.load() opens the app database and restores the app-database snapshot first. If that snapshot is missing, it reads the portable generated backup at .lapis/cache/metadata-cache.json, validates the versioned envelope, applies the snapshot, and hydrates AppDatabase with both the snapshot and per-file indexed metadata records. Legacy ScopedVaultStore metadata snapshots remain a final migration fallback. When no valid state exists, metadata loading runs the first-open rebuild and emits loaded only after that rebuild persists a fresh snapshot and backup.

Current implementations:

  • SqliteWasmAppDatabase — SQLite WASM using OPFS persistence for browser/PWA sessions when available. The public class proxies all SQLite execution through a dedicated worker while mirroring app state in the main thread, and search-document upserts now sync the worker-prepared document back into that mirror instead of re-running search preparation on the renderer.
  • IndexedDbAppDatabase — browser fallback backed by ScopedVaultStore(vaultId, "app-database").
  • MemoryAppDatabase — tests and runtimes without IndexedDB.
  • NativeDesktopAppDatabase — native SQLite generated-state contract for the Electron desktop host.

In browser bundles, the SQLite WASM implementation resolves sqlite3.wasm through the package’s exported asset URL so bundlers serve the binary instead of falling through to an HTML route.

The SQLite schema includes schema_meta, files, metadata, links, tags, properties, search_docs, and search_fts. SQLite is not canonical note storage; deleting the app database should only require rebuilding metadata and search from files exposed by the vault adapter. Search queries return document hits plus explicit snippet/range metadata and score explanations so UI layers do not need a separate tokenization/highlight engine. The search contract now also exposes incremental search-index stats plus explicit beginSearchIndexingBatch() / endSearchIndexingBatch() hooks so refresh-heavy backends can batch expensive persistence work. In browser/PWA sessions, SQLite ownership is coordinated per vault with Web Locks and BroadcastChannel heartbeats; only the owner tab starts the SQLite worker, while secondary tabs proxy AppDatabase requests to that owner when delegation is available and fall back to the blocked state only when delegation cannot be established. The shared Transformers.js embedding provider keeps browser-managed Cache API caching in browser/PWA runtimes, but disables that cache path inside native desktop renderers so corrupted persisted Chromium cache state cannot wedge semantic-search initialization.

Browser Adapters

  • OpfsVaultAdapter — Origin Private File System. Works offline, no permission prompts. Default for new browser-local vaults.
  • BrowserHandleVaultAdapter — File System Access API. Opens user-selected real folders via picker. Requires transient user activation.
  • OPFS and File System Access handle-backed browser adapters retry recoverable NotFoundError, NotReadableError, NoModificationAllowedError, InvalidStateError, and ENOENT failures on reads, writes, deletes, and directory listings, and normal writes truncate existing files before replacing their contents.
  • getAdapterVaultId(adapter) — Returns a stable identifier for scoped storage.
  • ScopedVaultStore(vaultId, scope) — Scoped browser storage still used for vault profiles, legacy metadata fallback, and the IndexedDB app-database fallback.

File Model

TAbstractFile { path, parent, vault, baseName, extension, name }
├── TFile { stat: FileStats {ctime, mtime, size} }
└── TFolder { children: TAbstractFile[], iterateAll(cb) }

TAbstractFile.copy(props) creates a clone with property overrides.

Vault Class

Extends EventDispatcher. Owns the file tree:

  • adapter: DataAdapter — Active backend.
  • files: Record<string, TAbstractFile> — In-memory file tree cache.
  • cache: FileCache — LRU in-memory cache.
  • configDir: string".obsidian".

File Operations: create, createBinary, createFolder, mkpath (recursive), modify, modifyBinary, process (read-modify-write), delete, trash, rename, copy.

Tree Operations: load, loadPath, reload, getRoot, getAbstractFileByPath, getFileByPath, exists, stat, list.

Events: load, create, modify, delete, rename, all (any event with type discriminator).

DirectoryWatcher

Polls or observes the adapter for changes: watch(path, opts){close()}. Events: create, modify, delete, error, all.

Path Utilities

normalizePath, joinPath, splitPath, dirname, basename — all browser-safe, no Node.js dependency.


MetadataCache — File Indexing

The MetadataCache (in cache.svelte.ts) indexes every file’s structural content.

CachedMetadata

interface CachedMetadata {
  frontmatter?: FrontMatterCache;
  frontmatterPosition?: Pos;
  links?: LinkCache[]; // [[wikilinks]]
  embeds?: EmbedCache[]; // ![[embeds]]
  tags?: TagCache[]; // #tags
  headings?: HeadingCache[]; // Headings with level (1-6)
  sections?: SectionCache[]; // Block-level sections
  listItems?: ListItemCache[]; // List items with optional task state
  footnotes?: FootnoteCache[]; // [^ref] footnotes
  blocks?: Record<string, BlockCache>; // ^block-ids
}

Every cache item carries a Pos { start: Loc, end: Loc } where Loc = { line, col, offset }.

Cache Items

TypeFields
HeadingCacheheading: string, level: 1-6
LinkCachelink, original, displayText?
TagCachetag
FootnoteCacheid
BlockCacheid
ListItemCacheid?, task?, parent: number
SectionCacheid?, type
  • getFirstLinkpathDest(linkpath, sourcePath) — Resolves [[link]] to a TFile. Tries exact path, .md suffix, .markdown suffix, then basename match.
  • resolvedLinks and unresolvedLinks — Adjacency maps: Record<string, Record<string, number>>.
  • resolveSubpath(cache, subpath) — Resolves #heading, ^block, [^footnote] references within a file.

Metadata Processors

Plugins register custom MetadataProcessor objects per file extension:

interface MetadataProcessor {
  read(data: string, ctx: { cache; file }): Promise<CachedMetadata>;
  write(cache: CachedMetadata): string;
}

Persistence

Caches are persisted through app.appDatabase keyed by vault ID. File tracking by {mtime, size, hash} is preserved, and parsed metadata is also decomposed into app-database file, link, tag, and property records. Persistence remains debounced at 500 ms. A legacy ScopedVaultStore cache can be read as a migration fallback when no app-database snapshot exists.

Events: changed(file, data, cache), deleted(file, prevCache), loaded.


MetadataTypeManager

Tracks frontmatter property types across all files:

  • types: Record<string, MetadataTypeDef> — Persisted to types.json in config folder.
  • properties: Record<string, MetadataTypeProperty> — Runtime index with { name, type, count, files: Set<string> }.
  • registeredTypeWidgets: Record<string, TypeWidget> — Plugin-provided custom renderers.

Type Inference: Number, Boolean, Tags, Aliases, Date (YYYY-MM-DD), DateTime (ISO 8601), Multitext (primitive arrays), Array, Object, Text (fallback).

Methods: load(), save() (debounced), setType(field, type), registerTypeWidget(widget), getAllProperties(), getValues(key), processChange(file, cache), processDelete(file), trackChanges(), plus exact top-level bulk rename, delete, and type-change helpers used by All Properties. Exact-key bulk helpers preserve literal dotted keys such as note.status instead of treating them as lodash paths.


Editor — CodeMirror 6 Integration

The Editor class (in editor.svelte.ts) wraps a CodeMirror 6 EditorView:

  • view: EditorView — CM6 instance.
  • file: TFile | null — Backing file.
  • extensions: Extension[] — Active extensions.
  • id: string — Unique ID (UUID).
  • getValue() / setValue(content) — Full document access.
  • replaceContent(content) — Efficient partial replace via dispatch.
  • getLine(line) — Gets line text.
  • somethingSelected() — Has selection?
  • save() — Writes to vault (debounced 500 ms).
  • refresh() — Requests CM6 measure/layout.
  • updateExtensions(extensions?, context?) — Reconfigures CM6 extensions.
  • trackChanges(callback?) — Syncs external file changes to the editor; returns unsubscribe.

Events: change(data: string).

The shared NoteEditor renders its inline file title inside the editor scroll area immediately before the .cm-editor-content editor host. Desktop visibility follows appearence.interface.showInlineTitle, while mobile workspace display mode forces the same in-scroll title on so file-backed mobile titles scroll with the document instead of separate shell chrome.

The shared class-based syntax highlighter used by API-owned editors emits the existing CodeMirror cm-* token classes together with compatible hljs-* token classes for tags that map cleanly onto the bundled highlight.js theme vocabulary. This lets non-markdown editors reuse highlight.js token styling without dropping the existing CodeMirror selectors.

CM6 State Fields

State fields make app context available inside extensions:

  • editorViewField: StateField<MarkdownFileInfo>{ app, file, editor }.
  • editorEditorField: StateField<EditorView> — Editor instance.
  • editorLivePreviewField: StateField<boolean> — Live preview mode flag.

Language Services

App.languageServices owns provider registration and serializable document routing for editor-adjacent language features. Providers implement the shared LanguageServiceProvider contract with metadata (id, languages, runtime, priority, and capabilities) plus optional diagnostics, completion, hover, definition, code-action, and dispose hooks. Consumers pass VirtualDocument payloads containing only URI, language id, version, and text, plus serializable positions/ranges and app-contributed global declaration snapshots.

For a given { languageId, capability } pair providers are consulted in descending priority; diagnostics and code-action helpers merge outputs only from providers that share the maximal priority among registered matches so a host-native Electron sidecar (priority > 0) excludes duplicate diagnostics from bundled worker backends without unregistering them. Completions stop at the earliest provider that yields items; hover behaves similarly while definitions bail out once a provider responds with locations.

The API package also exports CodeMirror adapters under @lapis-notes/api/editor/language-service:

  • diagnostics through async lint integration via lapisCodeMirrorLint() and enriched mapToLapisLintDiagnostic() output (Svelte lapis-lint-tooltip with markdownlint rule links, copy, View Problem, language-service code actions), plus optional lintGutter()
  • completions through @codemirror/autocomplete, with shared defaults and tooltip theming alignment via lapisCodeMirrorAutocomplete() (root export @lapis-notes/api and extensions/autocomplete) composed with @lapis-notes/ui/codemirror-autocomplete.css
  • lint tooltip and inline-problem chrome through workspace-loaded CodeMirror CSS (packages/workspace/src/lib/styles/codemirror-lint.css); inner hover layout lives in the Svelte component under extensions/lint/
  • vault-driven completion helpers (vaultFilesMatchingPathSubstring, media filters, isImageEmbedLinkPath) for wiki-link sources in first-party plugins
  • hover through hoverTooltip
  • languageServiceDocumentContext, a Facet for non-file-backed editors such as notebook cell editors that need to resolve a virtual document and map positions/ranges

Adapters only call app.languageServices; markdownlint, TypeScript, future Electron-native providers, and LSP transports stay behind the provider contract.


Supporting Systems

Internationalization

LocalizationManager wraps i18next:

  • t(namespace, key, params?) / t(key, params?) — Translate with flexible signature.
  • plural(count, ns, key, params?) — Plural forms.
  • setLocale(locale) — Switch language.
  • addResourceBundle(ns, locale, translations, deep?, overwrite?) — Load translations.
  • currentLocale, availableLocales — Reactive state.

Search Utilities

  • prepareFuzzySearch(query)(text) => SearchResult | null — Tries simple match first, falls back to fuzzy.
  • prepareSimpleSearch(query) — Substring only.
  • SearchResult: { score: number, matches: [start, end][] }.
  • renderMatches(el, text, matches) / renderResults(el, text, result) — Highlights match ranges in DOM.
  • useTextHighlight(el, { query, value }) — Svelte action that highlights literal query text with text nodes and suggestion-highlight spans without assigning raw HTML.
  • sortSearchResults(results) — Sorts by score descending.
  • SuggestModal<T>, FuzzySuggestModal<T> — Modal bases for fuzzy search UIs.

History

HistoryManager<T> — Navigation stack:

  • pushState(state) / replaceState(state) / updateState(partial).
  • back() / forward() / go(steps).
  • hasForward / hasBackward — Computed navigation availability.
  • maxSize: number — Default 20.

Menu with MenuItem children:

  • addItem(callback), addSeparator(section?), setTitle(title), setSection(section).
  • showAtPosition({x, y, width?, overlap?, left?}), popover(), dropdown().
  • setOpen(open), hide(), and close() synchronize renderer menu state and run onHide when an open menu transitions closed.
  • search(value) — Filter items.
  • MenuItem supports: setTitle, setIcon, setChecked, setDisabled, setWarning, setSection, onClick.
  • Shared menu rendering trims separators that would otherwise land at the start or end of the rendered menu after dynamic construction or filtering.
  • Shared context, dropdown, and popover renderers use the same drawer-backed presentation on mobile-width viewports while preserving desktop popup behavior.

Popover

HoverPopover (extends Component) with lifecycle states: Hidden → Showing → Shown → Hiding.

DOM Enhancements

enhance.ts patches Node and HTMLElement prototypes:

  • Node: detach, empty, insertAfter, appendText, createEl, createDiv, createSpan, createSvg, doc, win.
  • Element: find, findAll, getText, setText, addClass, removeClass, toggleClass, hasClass, setAttr, getAttr, matchParent, getCssPropertyValue.
  • HTMLElement: show, hide, toggle, toggleVisibility, on (delegated events), off, onClickEvent.

Embed Registry

Maps file extensions to embed view constructors:

class EmbedRegistry {
  embedByExtension: Record<string, EmbedView>;
  register(extension: string, view: EmbedView): () => void;
  get(extension: string): EmbedView | null;
}
type EmbedView = (props: {
  app;
  containerEl;
  state;
}) => void | { destroy?: () => void | Promise<void> };

Bundled and community plugins use this registry to supply plugin-owned inline renderers for markdown file embeds without widening the shared HTML sanitization rules. The markdown package looks up a renderer by file extension for non-image embeds and, when one exists, mounts it into the embed container and runs the returned destroy handle on teardown.

Icon System

  • registerIconPacks(loaders: IconLoader[]) — Registers icon sets.
  • getSvg(iconName, customisations?) — Returns SVG element. Built-in Lucide icons (24px, 150+).
  • Prefixed lookup: lucide:name, custom:name. Prefix optional for Lucide.
  • IconLoader: { name, loader? | icons? } — Async or inline icon packs.
  • parseCodiconLabel(label) — Splits declarative label text into text, codicon, and registry icon segments for $(token) syntax.
  • isLabelIconAvailable(iconName) — Cached async lookup for registry icons used during label rendering fallback.
  • Label tokens: unqualified $(play) (codicon font, then registry fallback), qualified $(lucide:file-text) (registry SVG only), optional ~spin modifier. Malformed tokens remain literal text in the rendered label.

Utilities

  • apiVersion: "1.12.3" — Current API version.
  • cn(...inputs) — Tailwind class merging (clsx + tailwind-merge).
  • debounce(cb, timeout?, resetTimer?) — Returns Debouncer with cancel() and run().
  • uniqueId(salt?) / md5(string) — MD5-based hashing.
  • requireApiVersion(version) — Semver compatibility check.
  • stripHeadingForLink(text) — Normalizes heading text for anchor matching.

Tasks and Workers

  • Tasks — Promise collection: add(cb), addPromise(p), isEmpty(), promise() waits for all.
  • PromiseWorker — Wraps Worker with typed request/response. postMessage<T>(type, data, timeout?) returns a promise (default 10 s timeout). Message format: {id, type, data}{id, success, result?, error?}.

Markdown Rendering

  • MarkdownPostProcessor: (el, ctx) => Promise<void> | void — Sorted by sortOrder (default 0).
  • MarkdownPostProcessorContext: { docId, sourcePath, frontmatter, addChild(child), getSectionInfo(el) }.
  • MarkdownRenderChild (extends Component) — Lifecycle wrapper for rendered elements.
  • MarkdownRenderer.render(app, markdown, el, sourcePath, component) — Static rendering API.
  • MarkdownPreviewRenderer — Static registration for post-processors.

Settings UI Components

settings.svelte.ts provides a builder API for settings panels:

Components: TextComponent, ToggleComponent, SliderComponent, SelectComponent, ButtonComponent, ExtraButtonComponent, ProgressBarComponent, DropdownComponent, TextAreaComponent, ColorPickerComponent, IconPickerComponent.

Structure: AppSettingsSettingGroupSetting → components. Each setting has setName, setDesc, addToggle, addText, addSlider, addSelect, addButton, addColorPicker, etc.

Modals: Modal base with open(), close(), onOpen(), onClose(), containerEl.

SettingTab: Abstract class with display() and containerEl for custom settings panels.

Compatibility Surface

compat.ts provides:

  • SecretComponent — Password input field.
  • Protocol types: CliHandler, CliFlags, ObsidianProtocolHandler, ObsidianProtocolData.
  • moment — Luxon-compatible date library shim for Obsidian API compatibility.