Skip to content

Plugin development guide

Eidos Lite plugins add document formatters, command-palette actions, and custom interfaces to a Space. Plugins run in a local, isolated sandbox without requiring network access or external CDNs.

A plugin can combine any of these extension points in a single plugin.json manifest:

Your feature Extension point User entry point
Format Markdown or text Formatter provider Format Document or Format Document With…
Count words or run a document operation Action Cmd+K, optionally a custom shortcut
Edit CSV, Markdown, or custom files Document view File context menu → Open with
Open an independent plugin page Page view Space plugin navigation entry
Show table records on a map or chart Table view Table view menu → New view

Quickstart: Create and run your first plugin

Section titled “Quickstart: Create and run your first plugin”

Ensure you have Node.js 22.12 or newer and a package manager (npm, pnpm, or yarn) installed. Also install Eidos Lite for running and testing your plugins locally.

Choose either method to create a new plugin project:

Method 1: Using npm / npx (Recommended, zero extra CLI setup)

Terminal window
npx @eidos.space/plugin-tools create my-plugin
cd my-plugin
npm install # or pnpm install

Method 2: Using the Eidos CLI

Terminal window
eidos plugin create my-plugin
cd my-plugin
pnpm install

The generated project has the following layout:

my-plugin/
├── plugin.json # Manifest declaring capabilities and entry points
├── package.json # Dependencies and npm scripts
├── tsconfig.json # Strict TypeScript configuration
└── src/
├── main.ts # View mount entry point
├── csv.ts # Business logic
└── style.css # Styles

The scaffold pre-configures official dependencies and scripts in package.json:

  • @eidos.space/plugin-sdk: TypeScript types and interfaces for the Plugin 1.0 specification.
  • @eidos.space/plugin-tools: CLI providing eidos-plugin check (validation) and eidos-plugin pack (self-contained bundle packaging).
  • npm run check: Runs eidos-plugin check . for static verification.
  • npm run pack:plugin: Runs eidos-plugin pack . to produce an offline .eidos-plugin bundle.

You don’t need a manual build step to test your plugin. Eidos Lite compiles source files on the fly:

  1. Launch Eidos Lite and open any Space.
  2. In the top toolbar, click the Plugins icon (between Search and the Space menu).
  3. Click Load development source… and select your project’s plugin.json (or a standalone .ts file).
  4. Review the declared capabilities and click Enable for this Space.
  5. Create or select a .csv file in your Space, right-click it, and choose Open with → CSV Table.

When you edit code in src/, Eidos Lite automatically recompiles and hot-reloads the view!


The manifest declares your plugin’s identity, permissions, and extension points:

{
"apiVersion": 1,
"id": "example.my-plugin",
"name": "My Plugin",
"version": "0.1.0",
"extension": "./src/extension.ts",
"views": [],
"actions": [],
"formatters": [],
"placements": []
}
  • apiVersion: Currently 1, specifying the Plugin 1.0 host contract.
  • id: Globally unique identifier (e.g. vendor.feature).
  • extension: Points to background controller code that runs on activation.
  • views: List of custom UI views.
  • placements: Tells the host where to display views and actions in the UI.

The core SDK provides TypeScript types only:

import type { ExtensionContext, ViewContext } from "@eidos.space/plugin-sdk"

All runtime APIs are injected by the host into the ctx context parameter. There is no SDK bundle overhead and no connect() call.

  • Background lifecycle: For actions and formatters, extension exports a default activate(ctx: ExtensionContext) function.
  • View lifecycle: Each view’s entry exports a default mount(ctx: ViewContext, root: HTMLElement) function.
  • Disposal: Clean up event listeners and timers by returning { dispose() { ... } } or calling ctx.subscriptions.add(...).

Plugins install once per device and are enabled per Space:

  • Eidos Lite and eidos serve store plugins in ~/.eidos/plugins (%USERPROFILE%\\.eidos\\plugins on Windows).
  • Set EIDOS_HOME to relocate the store directory.
  • The store contains immutable package archives (packages/<sha256>.eidos-plugin), device configuration (config.json), and per-plugin offline data under data/<plugin-id>/.

Branding and functional icons (Icons & Branding)

Section titled “Branding and functional icons (Icons & Branding)”

The plugin architecture cleanly separates plugin brand logos from functional view/action icons:

  • Plugin Logo (manifest.icon): Represents the overall plugin brand. Displayed in the Marketplace listing, plugin details pane, and Space plugin settings.
  • View/Action Icons (view.icon / action.icon): Represents a specific capability or view (e.g. mind map, grid, formatter). Displayed in the file context menu (Open with), editor view tabs, and Command Palette. If view.icon is omitted, the host automatically falls back to manifest.icon; if neither is provided, an elegant theme-aware initial badge is generated.

Icons support three declaration formats:

{
"icon": {
"paths": ["M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"]
}
}
  1. Inline monochrome SVG paths (recommended): { "paths": ["..."] }, rendered using the host’s design system text color (currentColor) with pixel-perfect alignment and zero overhead.
  2. Local relative file paths: e.g. "./assets/logo.svg" or "./assets/view.png". During eidos-plugin pack, the bundler automatically reads and inlines the asset as a Base64 Data URL to preserve offline self-containment.
  3. Data URL string: e.g. "data:image/svg+xml;base64,...".

File associations and global default editors

Section titled “File associations and global default editors”

By declaring location: "file/open" in placements, a view registers as an editor for specific file extensions:

"placements": [
{ "location": "file/open", "view": "table", "extensions": [".csv"] }
]
  • Open with menu: Right-click a .csv file in the Space file tree and hover over Open with to see the plugin editor with its dedicated functional icon.
  • Global default editor settings: In Eidos Lite preferences (Settings → Files), all installed plugins and their associated file extensions are listed alongside Markdown and HTML. Users can choose their preferred default editor for .csv or custom file types globally, opening files directly on double-click.

A formatter processes document text and returns updated text. The host manages undo/redo, selections, and saving.

In plugin.json:

{
"apiVersion": 1,
"id": "example.typography",
"name": "Typography",
"version": "0.1.0",
"extension": "./src/extension.ts",
"formatters": [
{
"id": "trim",
"title": "Trim trailing whitespace",
"extensions": [".md", ".txt"]
}
]
}

In src/extension.ts:

import type { ExtensionContext } from "@eidos.space/plugin-sdk"
export default function activate(ctx: ExtensionContext) {
ctx.formatters.register("trim", {
format({ text, signal }) {
signal.throwIfAborted()
return { text: text.replace(/[ \t]+(?=\r\n|\r|\n|$)/g, "") }
},
})
}

Users run formatting via:

  • Cmd+KFormat Document
  • Default shortcut: Shift+Option+F (macOS) / Shift+Alt+F (Windows)

Actions execute custom logic from the Command Palette (Cmd+K) or keyboard shortcuts.

In plugin.json:

{
"apiVersion": 1,
"id": "example.text-tools",
"name": "Text Tools",
"version": "0.1.0",
"extension": "./src/extension.ts",
"actions": [
{
"id": "count",
"title": "Count characters",
"context": "document",
"access": "read",
"extensions": [".md", ".txt"]
}
],
"placements": [
{ "location": "command-palette", "action": "count" },
{ "location": "keybinding", "action": "count", "key": "Mod+Alt+C" }
]
}

In src/extension.ts:

import type { ExtensionContext } from "@eidos.space/plugin-sdk"
export default function activate(ctx: ExtensionContext) {
ctx.actions.register("count", async ({ binding, ui }) => {
if (binding.kind !== "document") return
const snapshot = await binding.document.read()
await ui.notify(`Characters: ${Array.from(snapshot.text).length}`)
})
}

Document views replace the default editor for specific file extensions when selected in Open with.

In plugin.json:

{
"apiVersion": 1,
"id": "example.markdown-viewer",
"name": "Markdown Preview",
"version": "0.1.0",
"views": [
{
"id": "preview",
"title": "Rich Preview",
"entry": "./src/preview.ts",
"context": "document",
"access": "write"
}
],
"placements": [
{ "location": "file/open", "view": "preview", "extensions": [".md"] }
]
}

In src/preview.ts:

import type { ViewContext } from "@eidos.space/plugin-sdk"
export default async function mount(ctx: ViewContext, root: HTMLElement) {
if (ctx.binding.kind !== "document") return
const file = ctx.binding.document
// Subscribe to changes from the host or other editors
const observation = await file.observe((snapshot) => {
root.textContent = snapshot.text
})
ctx.subscriptions.add(observation.subscription)
// Initial render
root.textContent = observation.snapshot.text
return {
dispose() {
root.replaceChildren()
},
}
}

To edit the document:

const before = await file.read()
const result = await file.edit({
text: nextText,
expectedVersion: before.version,
label: "Edit cell",
})
if (result.status === "stale") {
// Conflict reconciliation
}

Independent pages add navigation entries to the Space sidebar:

In plugin.json:

{
"views": [
{
"id": "home",
"title": "Dashboard",
"context": "page",
"entry": "./src/page.ts"
}
],
"placements": [{ "location": "navigation", "view": "home" }]
}

In src/page.ts:

import type { ViewContext } from "@eidos.space/plugin-sdk"
export default function mount(ctx: ViewContext, root: HTMLElement) {
if (ctx.binding.kind !== "page") return
root.innerHTML = `
<div style="padding: 24px;">
<h1>Plugin Dashboard</h1>
<p>Current route: ${ctx.binding.route || "/"}</p>
</div>
`
return {
dispose() {
root.replaceChildren()
},
}
}

Table views render .eidos table records in novel layouts like charts, maps, or galleries.

In plugin.json:

{
"views": [
{
"id": "map",
"title": "Map View",
"entry": "./src/map.ts",
"context": "table",
"access": "read"
}
],
"placements": [{ "location": "table/view", "view": "map" }]
}

In src/map.ts:

import type { ViewContext } from "@eidos.space/plugin-sdk"
export default async function mount(ctx: ViewContext, root: HTMLElement) {
if (ctx.binding.kind !== "table") return
const table = ctx.binding.table
// Read view metadata and column definitions
const meta = await table.read()
// Fetch paginated rows with the host's current filters and sorting
const page = await table.getPage({ offset: 0, limit: 100 })
// Accelerated SQLite server-side aggregation
const aggregates = await table.aggregate({
metric: { op: "count" },
groupBy: { fieldId: "category" },
})
// Listen for table mutations
const unsubscribe = await table.observe(() => {
// Re-render when records change
})
ctx.subscriptions.add(unsubscribe)
return {
dispose() {
root.replaceChildren()
},
}
}

You can use React, Vue, Svelte, or vanilla DOM. To use React:

Terminal window
pnpm add react react-dom
pnpm add -D @types/react @types/react-dom

In src/main.tsx:

import { createRoot } from "react-dom/client"
import type { ViewContext } from "@eidos.space/plugin-sdk"
import { App } from "./app"
import "./style.css"
export default function mount(ctx: ViewContext, element: HTMLElement) {
const root = createRoot(element)
root.render(<App context={ctx} />)
return {
dispose() {
root.unmount()
},
}
}

Use the host’s semantic CSS variables to automatically match light and dark modes:

.card {
background-color: var(--e-color-bg-subtle);
color: var(--e-color-text-primary);
border: 1px solid var(--e-color-border-subtle);
border-radius: var(--e-radius-md);
}

Offline sandbox & third-party library best practices

Section titled “Offline sandbox & third-party library best practices”

Eidos plugins execute inside an isolated, network-gapped iframe sandbox. When integrating third-party UI libraries (such as @glideapps/glide-data-grid, chart renderers, or rich text engines), keep these practices in mind:

  1. Always explicitly import library CSS: Many modern libraries (using Linaria, Vanilla Extract, or standard CSS) store critical structural layout and positioning rules in separate .css files.
    • Always @import the library’s CSS at the top of main.tsx or style.css.
    • Missing core CSS can cause the host container to compute an effective height of 0px. Internal resize observers (e.g., ResizeObserver / useResizeDetector) will measure zero dimensions and skip mounting <canvas> or DOM trees entirely, resulting in “toolbar visible but content completely blank”.
  2. Clean ESM dependencies and no dynamic require:
    • The host bundler compiles code and dependencies into self-contained browser ESM modules.
    • Avoid CommonJS runtime require() calls in client code; use standard static import syntax.
  3. Handle document concurrency and stale edit conflicts:
    • Read the current snapshot and its version: const snapshot = await ctx.binding.document.read().
    • Apply edits with the expected version: await ctx.binding.document.edit({ text, expectedVersion: snapshot.version, label: "Edit cell" }).
    • If { status: "stale" } is returned, the document was modified by another editor or external process. Re-read the latest snapshot to reconcile or prompt the user, avoiding accidental overwrites.

Plugins have access to isolated, persistent key-value binary storage that survives restarts:

In plugin.json:

{
"storage": { "maxBytes": 536870912 }
}

In your code:

// Write data
await ctx.storage.write(
"config",
new TextEncoder().encode(JSON.stringify({ dark: true }))
)
// Read data
const bytes = await ctx.storage.read("config")

All external network requests must be explicitly declared in browser.networkOrigins. Direct eval, Node.js APIs, and CDN scripts are blocked by the sandbox CSP.

{
"browser": {
"networkOrigins": ["https://api.example.com"]
}
}

You can run scripts directly from your plugin directory or use the CLI:

Terminal window
# Validate manifest metadata, TypeScript types, and dependencies
npm run check
# Or run directly:
# npx eidos-plugin check .
# eidos plugin check .
# Bundle into an offline, self-contained .eidos-plugin archive
npm run pack:plugin
# Or specify a custom output path:
# npx eidos-plugin pack . --out ./dist/my-plugin-0.1.0.eidos-plugin

Packaging creates a <id>-<version>.eidos-plugin archive. This is a standardized gzip-compressed JSON envelope containing format: 1, full manifest metadata, and bundled module sources. Users can install it directly in Eidos Lite (Plugins → Install plugin…) without Node.js or an internet connection.

  1. Publish <id>-<version>.eidos-plugin as a GitHub Release asset in your repository.
  2. Submit a PR to the official registry updating plugins.registry.json with your plugin ID, version, release download URL, and SHA-256 checksum.
  3. Once merged, your plugin appears in the Eidos Lite Marketplace for all users.