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

  1. Create src/plugins/my-tool/ (folder name must match manifest id).
  2. Add manifest.json, panel.tsx, and docs.md.
  3. Run the app → Settings → Plugins → Refresh.
  4. 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

FieldTypeDescription
idstringStable slug; must match folder name; used in /tools/:id
namestringDisplay name
descriptionstringShort subtitle for search and cards
versionstringSemver, e.g. 1.0.0
categorystringSee categories below
iconstringLucide icon name (kebab-case), e.g. git-compareor use logo

Categories

data · development · encode-decode · text · time · other

Optional fields

FieldDescription
logoURL or path to custom image (replaces icon)
entryPanel module path (default ./panel.tsx)
colorAccent: purple, blue, green, orange, pink, yellow
keywordsSearch aliases
popularShow on Home “Popular Tools” when true
author, homepageMarketplace metadata (future)
permissionsFuture capability flags
minAppVersionMinimum host app version
projectDataOpt 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:

HookUse
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-0 flex/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 useMemo for 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:

  1. What it does — one short paragraph
  2. How to use — numbered steps
  3. 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)

ExportPurpose
PluginShellRoot flex column wrapper
PluginToolbarToolbar with start/center/end slots
ToolbarGroup / ToolbarDividerGrouped actions with visual separators
ValidationPillsValidity + stat chips above editor content
StatusBar / StatusBarItemFooter stats row
PaneHeaderResizable pane title bar
SectionCardBordered inspector block
ColumnHeaderGrid column title
SidebarLeft/right options rail
EmptyStateEmpty 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)

ModuleUse
plugin-handoff.tsOne-shot text transfer between tools via sessionStorage (JSON Editor, YAML Editor, File Editor, Remove Duplicates)
use-flash-status.tsTimed status message for toolbar actions
utils.tscn() class merging
tabs.tsTab 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.

LibraryTypical use in plugins
@monaco-editor/react + monaco-editorSyntax-highlighted editors (JSON, SQL, XML, multi-language file editor)
papaparseCSV parsing
yamlYAML parse/stringify
prettierCode formatting (standalone build)
diffText diff
@noble/hashesSHA, BLAKE, etc.
crc-32CRC checksums
date-fnsDate/time formatting and math
zodInput validation schemas
react-hook-form + @hookform/resolversComplex forms (API client)
react-resizable-panelsEditor + inspector splits
react-markdown + remark-gfmRender markdown in-panel
lucide-reactIcons
zustandGlobal app state (useApp) — prefer local state in panels when possible
sonnerToast notifications
cmdkCommand 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/react Editor component.
  • Define themes once using surface colors from a local theme-colors.ts (see json-editor, sql-formatter).
  • For read-only output or small payloads, use a <pre> block instead of Monaco (see csv-to-json JSON output threshold).
  • Debounce model updates on large documents; show ValidationPills / overlay only when work is async.

Performance guidelines

Input sizeSuggestion
Small (< 2–10 KB)Synchronous useMemo parse/format, 0 ms debounce
MediumShort 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

EnvironmentBehavior
Build / bootAll src/plugins/*/manifest.json bundled via import.meta.glob
Dev refreshServer scan of src/plugins/ + plugins-local/; validates manifests; registers new IDs with dynamic import
Production workersCannot 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>/ with manifest.json, panel.tsx, docs.md
  • id matches folder name; valid semver and category
  • icon or logo; keywords for 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.