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
| Property | Type | Purpose |
|---|---|---|
i18n | LocalizationManager | Internationalization |
session | VaultSession | undefined | Runtime/vault/database session resolved by the shell |
workspace | Workspace | Layout, leaves, view registry |
vault | Vault | File system access |
appDatabase | AppDatabase | Per-vault generated metadata/search/app state |
plugins | PluginManager | Plugin lifecycle |
pluginDistribution | PluginDistributionManager | Verified plugin registry, install, update, uninstall, and provenance state |
internalPlugins | compatibility facade | Enabled bundled-plugin lookup for Obsidian-style plugin compatibility |
scope | Scope | Root keyboard scope |
keymap | Keymap | Scope stack manager |
settings | AppSettings | Settings groups container |
editors | Map<string, Editor> | Open editors by ID |
editorSuggest | Map<string, EditorSuggestManager> | Autocomplete managers |
configuration | ConfigurationSchema | JSON-schema config system |
fileManager | FileManager | File 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), andclearAll()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 theplugin-install-promptfallback 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
| Method | Purpose |
|---|---|
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 |
Hover-Link and Window Compatibility
registerHoverLinkSource(id, info)stores hover-preview source registrations for plugin compatibility and supports unload cleanup through the plugin lifecycle helpers.openPopoutLeaf()andmoveLeafToPopout()now route through a host popout capability. Browser and Electron hosts open a separateWindow/Documentthat still renders the sharedWorkspaceWindowtree, 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
WorkspaceWindowentries tracknormal,collapsed,minimized, andmaximizeddisplay 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(): stringabstract getDisplayText(): stringgetState() / 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 throughapp.vault.modify().onLoadFile(file)— Reads throughapp.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— Parsedmanifest.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():
| Method | Purpose |
|---|---|
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
EnterandTabchoose the active itemEscapecloses 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 forrequire()injection.pluginsPath: string—"/.obsidian/plugins".adapter: DataAdapter— File system.
Lifecycle:
loadPlugins()— Ensures the plugin folder exists, reads installed provenance from.obsidian/installed-plugins.json, discovers external plugin manifests, readscommunity-plugins.json, then activates bundled plugins, configured official plugins, and configured community plugins before emitting"plugins-loaded".loadPlugin(path)— Loads a single external plugin. Parsesmanifest.json, resolves source/provenance from verified installed state or caller options, validates/classifies optionallapismetadata, 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 relativerequire()calls for local.js,.cjs, and.mjschunks 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 aPlugininstance. Emits"plugin-loaded"only for instantiated plugins.enablePlugin(id)— For instantiated plugins, callsplugin.enable(), injects registered bundled core-plugin CSS or reads externalstyles.cssif 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 tocommunity-plugins.jsonfor official/community external plugins orcore-plugins.jsonfor bundled/system plugins as appropriate. Emits"plugin-enabled"only for instantiated plugins.disablePlugin(id)— For instantiated plugins, callsplugin.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 byScopedVaultStore(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, andENOENTfailures 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
| Type | Fields |
|---|---|
HeadingCache | heading: string, level: 1-6 |
LinkCache | link, original, displayText? |
TagCache | tag |
FootnoteCache | id |
BlockCache | id |
ListItemCache | id?, task?, parent: number |
SectionCache | id?, type |
Link Resolution
getFirstLinkpathDest(linkpath, sourcePath)— Resolves[[link]]to aTFile. Tries exact path,.mdsuffix,.markdownsuffix, then basename match.resolvedLinksandunresolvedLinks— 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 totypes.jsonin 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 viadispatch.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 enrichedmapToLapisLintDiagnostic()output (Sveltelapis-lint-tooltipwith markdownlint rule links, copy, View Problem, language-service code actions), plus optionallintGutter() - completions through
@codemirror/autocomplete, with shared defaults and tooltip theming alignment vialapisCodeMirrorAutocomplete()(root export@lapis-notes/apiandextensions/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 underextensions/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 andsuggestion-highlightspans 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 System
Menu with MenuItem children:
addItem(callback),addSeparator(section?),setTitle(title),setSection(section).showAtPosition({x, y, width?, overlap?, left?}),popover(),dropdown().setOpen(open),hide(), andclose()synchronize renderer menu state and runonHidewhen an open menu transitions closed.search(value)— Filter items.MenuItemsupports: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~spinmodifier. 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?)— ReturnsDebouncerwithcancel()andrun().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— WrapsWorkerwith 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 bysortOrder(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: AppSettings → SettingGroup → Setting → 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.