Plugin Audit
A quick audit of what plugins can extend in Verql today, where the seams are sharp, and where they’re still aspirational.
Plugins are normal Node modules loaded into the main process at app start. They get a typed
PluginContextand contribute via registries. Their UI surfaces are declared inmanifest.jsonand rendered by the renderer process via shared resolver hooks.
Status legend
Section titled “Status legend”- ✅ Fully wired — declarative manifest + runtime API + UI consumes it
- 🟡 Partly wired — manifest entry exists, runtime stub exists, but the UI or storage layer doesn’t honour it yet
- ⛔ Not wired — only a placeholder in
manifest.json, no real impl
Extension points
Section titled “Extension points”Database adapters ✅
Section titled “Database adapters ✅”The original use case. A plugin can register a new driver:
ctx.drivers.register('clickhouse', { async connect(profile) { /* return DbAdapter */ }})…and the connection picker, schema browser, and query runner pick it
up automatically. All six bundled DB plugins use this:
postgresql, mysql, sqlite, mongodb, redis, snowflake.
Connection middleware ✅
Section titled “Connection middleware ✅”Wraps any driver’s connect call. Used by the ssh-tunnel plugin to open
a tunnel before delegating to the underlying driver.
ctx.drivers.registerConnectionMiddleware('ssh', { async wrap(profile, next) { const tunnel = await openTunnel(profile) return next({ ...profile, host: '127.0.0.1', port: tunnel.localPort }) }})Connection fields ✅
Section titled “Connection fields ✅”Add custom inputs to the connection form (e.g. an “Account” field for
Snowflake, or an “SSH key path” field for the tunnel). Declared in
manifest.json under contributes.connectionFields.
Exporters & importers ✅
Section titled “Exporters & importers ✅”Plugins register file-format readers/writers used by the toolbar
“Export…” and “Import…” actions. core-formats ships CSV / JSON /
JSON-Lines / SQL. Adding Parquet or Excel would be a new plugin.
Type mappers ✅
Section titled “Type mappers ✅”Declare how column types convert between dialects. e.g. mapping PG
jsonb → MongoDB Object. The plugin host walks the registry when
schemas are read or queries are written.
Completion providers ✅
Section titled “Completion providers ✅”SQL editor completions (column names, function signatures, dialect keywords). Each DB plugin contributes its own.
Commands ✅
Section titled “Commands ✅”Register handlers for command-palette entries and keybindings.
ctx.commands.register('do-thing', handler) — namespaced under the
plugin name to avoid collisions.
Panels ✅
Section titled “Panels ✅”Long-form custom UI in the sidebar, secondary sidebar, or bottom dock.
Declared in manifest.json, rendered as a React tree the plugin
provides via the UI registry.
Activity bar / status bar / toolbar / tabs / context menus ✅
Section titled “Activity bar / status bar / toolbar / tabs / context menus ✅”Smaller UI surfaces with their own contribution slots. Plugins
declare items in manifest.json and resolve their dynamic state
through ui.registerSlot / ui.registerResolver.
AI providers, tools, context providers ✅
Section titled “AI providers, tools, context providers ✅”A plugin can:
- Register a new LLM provider (e.g. AWS Bedrock, local Ollama)
- Register a tool the assistant can call (
runQuery,lookupDoc) - Register a context provider that injects relevant info into the prompt (e.g. “current schema”, “recent errors”)
bundled/ai is the reference implementation.
Settings contributions ✅
Section titled “Settings contributions ✅”Plugins declare their own settings entries in manifest.json. They
appear in the plugin’s own panel and optionally in a core
Settings category (Editor, Appearance, AI, …).
Services ✅
Section titled “Services ✅”A generic dependency-injection lane: any plugin can provide a typed
service, any other can consume or onAvailable. Used by the AI host
to wire providers ↔ tools without hard dependencies.
IPC + broadcast ✅
Section titled “IPC + broadcast ✅”A plugin can own typed IPC channels (ipc.handle('foo:bar', …)) and
broadcast events to all renderer windows. Channel types live in
@shared/ipc so the renderer gets type safety.
What’s partly wired
Section titled “What’s partly wired”Themes 🟡
Section titled “Themes 🟡”Manifest: declared. contributes.themes: [{ id, name, type }] is
in src/main/plugins/types.ts.
Runtime: no registry. Themes are hardcoded as CSS files under src/renderer/src/primitives/theme/themes/, imported into globals.css, and the available-themes list lives in @shared/settings.
What’s missing for full plugin theming:
- Extend
ThemeContributionto carry actual tokens (a structured object like{ '--color-bg-primary': '#0b0f16', ... }), not justid/name/type. - Add a
ThemeRegistryto the SDK. Plugins callctx.themes.register({ id, name, type, tokens })at activation. - Add an IPC channel
theme:listso the renderer can fetch plugin-contributed themes alongside the built-ins. ThemeProvidermerges built-in + plugin themes, and on theme switch injects the registered tokens into a<style data-theme-id>tag that targets[data-theme="<id>"].- Move the three built-in themes (
nightshift,lab,inkpaper) into abundled/core-themesplugin. They register via the SDK like any third-party theme would — eats the dogfood.
After that, a third-party plugin can ship Nord+, One Dark Pro,
Atom One, anything, with no host-side change.
Editor themes (Monaco) 🟡
Section titled “Editor themes (Monaco) 🟡”Today: mapping from app theme → Monaco theme is hardcoded in
lib/monaco-themes.ts.
Should: be derived from the same theme contribution above. When a
plugin registers a theme, it can also supply a Monaco token table; the
host installs it via monaco.editor.defineTheme at activation.
Per-plugin keybindings 🟡
Section titled “Per-plugin keybindings 🟡”Commands have an optional keybinding field in the manifest, but the
keybinding store currently lives entirely in user settings and isn’t
merged with plugin contributions. Fix: at activation, merge plugin
keybindings into the keybinding list (deduplicated, plugin entries
flagged so users can rebind).
What’s not wired
Section titled “What’s not wired”Drag-and-drop providers ⛔
Section titled “Drag-and-drop providers ⛔”Plugins can’t currently say “I handle dropped files of type X.” E.g.
dragging a .sqlite file onto the window doesn’t ask the SQLite
plugin to open it.
Result-grid cell renderers ⛔
Section titled “Result-grid cell renderers ⛔”Custom cell renderers (image preview, geo-shape, sparkline) would be a small contribution surface. The grid currently does its own type detection.
Custom welcome / empty-state widgets ⛔
Section titled “Custom welcome / empty-state widgets ⛔”The empty-state hero is a fixed component. A plugin like “AWS RDS discovery” might want to drop a tile there (“Connect to your AWS account”).
Background tasks / agents ⛔
Section titled “Background tasks / agents ⛔”A plugin can register commands, but there’s no lifecycle for
long-lived background workers (e.g. a “watch this table for changes”
worker that emits events). Today this would have to be home-rolled
inside a plugin via setInterval + broadcast.
Notification provider ⛔
Section titled “Notification provider ⛔”The toast store lives in the renderer. A plugin can broadcast its own “please show this toast” event, but there’s no typed API.
Localisation ⛔
Section titled “Localisation ⛔”All strings are English-only. Adding a plugin contribution surface for locale files would be a small addition.
Quick guide: how to write a plugin
Section titled “Quick guide: how to write a plugin”src/main/plugins/bundled/<name>/:
manifest.json # contributionsindex.ts # activate(ctx) entry pointmanifest.json:
{ "name": "my-plugin", "version": "0.1.0", "displayName": "My Plugin", "description": "What this does", "main": "./index.ts", "contributes": { "drivers": [{ "id": "myproto", "name": "My Protocol" }], "commands": [{ "id": "do-thing", "title": "Do the thing" }] }}index.ts:
import type { PluginContext } from '../../sdk'
export async function activate(ctx: PluginContext) { ctx.drivers.register('myproto', { /* adapter */ }) ctx.commands.register('do-thing', async () => { /* … */ })}
export async function deactivate() { // optional cleanup — anything pushed to ctx.subscriptions is // auto-disposed for you}Drop it in, restart the app, it lights up in the plugin pane.
Recommended priorities
Section titled “Recommended priorities”If you have an afternoon to push the plugin system forward, the highest value in order:
- Themes as real contributions (👈 unlocks a community theme ecosystem, the most-asked-for thing in DB clients).
- Result-grid renderers (small surface, big visible win).
- Drag-and-drop providers (makes “open a .sqlite file” feel native).
- Per-plugin keybindings (low effort, high QoL).
- Background tasks (the most ambitious; defer until something actually needs it).