Plugin development
Technical reference for building DevTools Alpha tools. For end-user help, see Application in the sidebar. For visual and layout rules, see Plugin UI design language.
Architecture overview
src/plugins/<id>/
manifest.json → bundled at build time (import.meta.glob)
panel.tsx → lazy-loaded React panel (default export)
docs.md → in-app user documentation
components/ → optional UI pieces
lib/ → optional pure logic (parse, format, hooks)
src/core/plugins/ → registry, discovery, validation, types
src/components/plugin-ui/ → shared panel chrome
src/components/ui/ → shadcn/Radix primitives
src/lib/ → cross-plugin utilities (handoff, tabs, flash status)
At build time, every src/plugins/*/manifest.json is registered as a built-in plugin and its panel is code-split. In development, Settings → Plugins → Refresh rescans src/plugins/ and plugins-local/ for new manifests.
Plugins must not import other plugins directly. Share code via @/lib/, @/core/, or extract reusable modules under @/components/.
Quick start
- Create
src/plugins/my-tool/(folder name must match manifestid). - Add
manifest.json,panel.tsx, anddocs.md. - Run the app → Settings → Plugins → Refresh.
- Open the tool from the sidebar or ⌘ K.
For experiments without committing, use plugins-local/<plugin-id>/ at the project root (gitignored). Same file layout as built-in plugins.
Manifest schema
Required fields
| Field | Type | Description |
|---|---|---|
id | string | Stable slug; must match folder name; used in /tools/:id |
name | string | Display name |
description | string | Short subtitle for search and cards |
version | string | Semver, e.g. 1.0.0 |
category | string | See categories below |
icon | string | Lucide icon name (kebab-case), e.g. git-compare — or use logo |
Categories
data · development · encode-decode · text · time · other
Optional fields
| Field | Description |
|---|---|
logo | URL or path to custom image (replaces icon) |
entry | Panel module path (default ./panel.tsx) |
color | Accent: purple, blue, green, orange, pink, yellow |
keywords | Search aliases |
popular | Show on Home “Popular Tools” when true |
author, homepage | Marketplace metadata (future) |
permissions | Future capability flags |
minAppVersion | Minimum host app version |
projectData | Opt in to Project Data sidebar files (see below) |
Example
{
"id": "diff-checker",
"name": "Diff Checker",
"description": "Compare text or code and view differences",
"version": "1.0.0",
"category": "data",
"icon": "git-compare",
"color": "green",
"keywords": ["diff", "compare", "git"],
"popular": true,
"entry": "./panel.tsx"
}
Validation lives in @/core/plugins/validate.ts. Manifest id must exactly match the folder name.
Project Data (optional)
Document-style tools can persist user files in a shared Project Data store (IndexedDB + sidebar file list). Add a projectData block to opt in:
"projectData": {
"extensions": [".json"]
}
For tools that accept any plain-text file (File Editor, Base64, Hash Generator):
"projectData": { "acceptsText": true }
Plugins without projectData do not show the sidebar section. Each plugin remembers its own active file independently.
Integration hooks
Import from @/core/project-data:
| Hook | Use |
|---|---|
useProjectFileSession({ pluginId, content, setContent, hydrated }) | Full control when the panel owns editor state, handoffs, or custom hydration |
usePluginProjectDocument({ pluginId, legacyStorageKey?, onHydrated? }) | Single text buffer wired to Project Data (returns content, setContent, fileSession, clearDocument, hydrated) |
useProjectDataHydrated() | Wait for the store to finish loading from IndexedDB |
useClearActiveProjectFile() | Reset to an Untitled buffer (e.g. after plugin handoff) |
migrateLegacyLocalStorageDocument(pluginId, key, createFile) | One-time import from an old localStorage document key |
Switch behavior: when the user picks another file in the sidebar, the current buffer is saved first — to the active file if bound, or as Untitled / Untitled 2 / … if the buffer was unsaved and non-empty. Debounced writes keep bound files in sync while editing.
Status bar: pass fileSession.displayName to your status footer so users see Untitled vs config.yaml.
Reference panels: json-editor/panel.tsx, file-editor/panel.tsx, csv-to-json/panel.tsx.
Panel component
panel.tsx must default-export a React component:
import type { PluginPanelProps } from "@/core/plugins/types";
import { PluginShell } from "@/components/plugin-ui";
export default function Panel({ pluginId }: PluginPanelProps) {
return (
<PluginShell>
{/* tool UI */}
</PluginShell>
);
}
Authoring rules
- Use shared plugin chrome from
@/components/plugin-ui/*— see Plugin UI design language. - Use UI primitives from
@/components/ui/*for controls (Button, Select, Switch, etc.). - Do not repeat plugin name or description inside the panel (tab + sidebar identify the tool).
- Panels render below the tab bar; use
min-h-0flex/grid so scroll stays inside panes. - Prefer client-only logic in the panel unless you add server capabilities later.
- Keep heavy work off the main thread: debounce large inputs, use
useMemofor parse/format, lazy-load Monaco only when needed.
Placeholder while scaffolding:
import type { PluginPanelProps } from "@/core/plugins/types";
import { PluginPlaceholderPanel } from "@/core/plugins/placeholder-panel";
export default function Panel(props: PluginPanelProps) {
return <PluginPlaceholderPanel {...props} />;
}
Recommended folder layout
src/plugins/my-tool/
manifest.json
panel.tsx # orchestration, layout, state wiring
docs.md # user-facing help (required)
components/ # presentational pieces
lib/
constants.ts # samples, thresholds, defaults
types.ts
parse.ts # pure functions (easy to test)
format.ts
hooks.ts # debounce, persistence, derived state
utils.ts
Split pure logic (lib/) from UI (components/, panel.tsx). Reference implementations: json-editor, csv-to-json, yaml-editor.
Plugin documentation (docs.md)
Each plugin needs docs.md beside its manifest. It is rendered in the Documentation tab when that tool is active.
Structure:
- What it does — one short paragraph
- How to use — numbered steps
- Tips — optional edge cases, limits, dialect notes
Do not duplicate app-wide keyboard shortcuts; they are appended automatically from @/lib/app-shortcuts.ts.
Shared UI (@/components/plugin-ui)
| Export | Purpose |
|---|---|
PluginShell | Root flex column wrapper |
PluginToolbar | Toolbar with start/center/end slots |
ToolbarGroup / ToolbarDivider | Grouped actions with visual separators |
ValidationPills | Validity + stat chips above editor content |
StatusBar / StatusBarItem | Footer stats row |
PaneHeader | Resizable pane title bar |
SectionCard | Bordered inspector block |
ColumnHeader | Grid column title |
Sidebar | Left/right options rail |
EmptyState | Empty editor placeholder |
Use useFlashStatus from @/lib/use-flash-status for transient Copy/Paste feedback (flash left of the related button group).
UI primitives (@/components/ui)
shadcn-style Radix wrappers: Button, Input, Textarea, Select, Switch, Tabs, Dialog, DropdownMenu, ContextMenu, Resizable (react-resizable-panels), ScrollArea, ToggleGroup, and others under src/components/ui/.
Styling tokens: bg-background, bg-card, border-border, text-muted-foreground, text-foreground. Accent colors for plugins: COLOR_CLASSES from @/core/plugins/colors.
Cross-plugin utilities (@/lib)
| Module | Use |
|---|---|
plugin-handoff.ts | One-shot text transfer between tools via sessionStorage (JSON Editor, YAML Editor, File Editor, Remove Duplicates) |
use-flash-status.ts | Timed status message for toolbar actions |
utils.ts | cn() class merging |
tabs.ts | Tab IDs, navigation helpers (rarely needed in panels) |
To hand off text to JSON Editor from your panel:
import { handoffToJsonEditor } from "@/lib/plugin-handoff";
import { useApp } from "@/store/app-store";
function openInJsonEditor(text: string) {
if (!handoffToJsonEditor(text)) return;
useApp.getState().openTool("json-editor");
navigate({ to: "/tools/$toolId", params: { toolId: "json-editor" } });
}
Add new handoff targets in plugin-handoff.ts following the existing take* / consume-on-open pattern.
Libraries available in the app
These dependencies are already installed — prefer reusing them over adding new packages.
| Library | Typical use in plugins |
|---|---|
@monaco-editor/react + monaco-editor | Syntax-highlighted editors (JSON, SQL, XML, multi-language file editor) |
papaparse | CSV parsing |
yaml | YAML parse/stringify |
prettier | Code formatting (standalone build) |
diff | Text diff |
@noble/hashes | SHA, BLAKE, etc. |
crc-32 | CRC checksums |
date-fns | Date/time formatting and math |
zod | Input validation schemas |
react-hook-form + @hookform/resolvers | Complex forms (API client) |
react-resizable-panels | Editor + inspector splits |
react-markdown + remark-gfm | Render markdown in-panel |
lucide-react | Icons |
zustand | Global app state (useApp) — prefer local state in panels when possible |
sonner | Toast notifications |
cmdk | Command palette (host app) |
Before adding a new dependency, check whether an existing library or a small pure function in lib/ is enough.
Monaco editor patterns
- Lazy-load via
@monaco-editor/reactEditorcomponent. - Define themes once using surface colors from a local
theme-colors.ts(seejson-editor,sql-formatter). - For read-only output or small payloads, use a
<pre>block instead of Monaco (seecsv-to-jsonJSON output threshold). - Debounce model updates on large documents; show
ValidationPills/ overlay only when work is async.
Performance guidelines
| Input size | Suggestion |
|---|---|
| Small (< 2–10 KB) | Synchronous useMemo parse/format, 0 ms debounce |
| Medium | Short debounce (50–200 ms) |
| Large (> 50 KB+) | Longer debounce, startTransition, optional progress overlay |
Persist user input with Project Data (@/core/project-data) when the tool has editable documents; keep plugin-specific options in localStorage if needed (e.g. CSV parse options). Debounce writes on large content.
Enable / disable
Settings → Plugins lists every plugin with an Enabled toggle. Disabled plugins are hidden from the sidebar and ⌘ K search but remain on disk.
Discovery and refresh
| Environment | Behavior |
|---|---|
| Build / boot | All src/plugins/*/manifest.json bundled via import.meta.glob |
| Dev refresh | Server scan of src/plugins/ + plugins-local/; validates manifests; registers new IDs with dynamic import |
| Production workers | Cannot read arbitrary disk; future marketplace will fetch manifest + ESM bundle from CDN |
Core modules: @/core/plugins/builtins.ts, discovery.ts, discovery.server.ts, registry.ts, validate.ts.
Checklist
- Folder
src/plugins/<id>/withmanifest.json,panel.tsx,docs.md -
idmatches folder name; valid semver and category -
iconorlogo;keywordsfor search - Panel follows a layout archetype from Plugin UI design language
- Uses
@/components/plugin-ui/*for toolbar, status, validation, empty states - Heavy logic in
lib/; no cross-plugin imports - Settings → Refresh → tool appears in sidebar
- Opens in a tab; ⌘ K finds it; Documentation tab shows
docs.md
Lucide icons
Common names: braces, network, git-compare, database, lock, code-2, asterisk, hash, file-code, file-json, file-text, key-round, type, eraser, palette, calendar, timer, repeat. Unknown names fall back to a generic icon via @/core/plugins/icon-resolver.ts.