Skip to content

feat(plugins): a smaller plugin core, in 11 commits that each stand alone as an upstream PR - #29

Draft
ashx-j wants to merge 11 commits into
mainfrom
feat/plugins-core
Draft

ashx-j wants to merge 11 commits into
mainfrom
feat/plugins-core

Conversation

@ashx-j

@ashx-j ashx-j commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

#28 was too big to review: about 30,000 lines, with hooks into turns, ways to redraw T3's parts, and APIs that reach into threads and projects. This is a smaller first version of plugins, split so each commit can go upstream as its own PR.

A plugin is a folder with a t3-plugin.json manifest. In this version it can:

  • Ship themes and styles/ CSS. The main areas have stable data-t3-region names to aim CSS at.
  • Run server code with RPC methods and channels, settings with secrets in the secret store, storage, and MCP tools for agents.
  • Run a web half that adds right panels, command palette and keybinding commands, and its own settings panel.

You install a plugin by dropping its folder on Settings → Plugins, which shows one environment at a time. A new plugin installs turned off, so turning it on is the confirm step. A plugin that stops the server stays off after the restart. A second folder with a plugin id that's already in use doesn't run, and its row says which folder has the id.

Thread and project APIs, message and turn hooks, chat UI slots, redrawing T3's parts, agent skills, git and npm installs, and providers are held back for a follow-up branch. #28 still has all of it.

Commits, one per proposed upstream PR

# Commit Changed lines
1 Published themes paint from the first frame (no plugin code) 654
2 A plugin host publishes plugin themes, and Settings → Plugins turns plugins on and off 2,085
3 Plugins restyle the app with a styles/ folder, and each browser picks where plugin UI loads from 1,333
4 Install a dropped plugin folder over HTTP, and remove or unlink plugins 1,777
5 Plugins run server code, and the SDK package starts 2,135
6 Plugin storage and settings, with secrets in the secret store 1,980
7 Settings draws a plugin's settings form from its JSON Schema 836
8 Agents get plugins' MCP tools 857
9 Web halves load with T3's React and UI kit, and add right panels 2,059
10 Web halves add atoms, commands, a settings panel and assets, and the SDK gains theme tokens and a quick-notes example 1,235
11 The SDK builds into a package plugin authors can install 183

Each commit message is that PR's description, with a surface walk. The whole branch is 180 files and +14,000 / -115, about 4,100 of it tests and fixtures.

Screenshots

Settings → Plugins for one environment. Quick notes was just added, so it's off until turned on:

Plugins page with 4 plugins, Quick notes switched off

Quick notes as a right panel. The navy sidebar, the dark composer and its orange outline come from 3 CSS-only plugins:

A thread with the Quick notes right panel open

Notes

  • With no plugins installed, the startup chunks gain about 11 KB of minified plugin code (about 4.5 KB gzipped) in 5 more chunks, and a 2 KB watcher loads once the app is idle.
  • Server code needs Node 24 or later. Below that, a plugin's server half fails as incompatible, and its themes, styles and web half still run.
  • An agent's call to a plugin tool fails after 5 minutes, and a plugin registers at most 32 tools.
  • The mobile app shows no plugin UI.
  • Every commit passed vp run check:changed locally. One ChatView browser test times out under the full parallel run and passes alone. I checked Settings → Plugins, installing a plugin and the Quick notes panel in a browser before the latest fixes, which weren't checked in a browser. The fork's CI can't run, because it needs upstream's runners.

pr filed by Claude Opus 5.5 in Claude Code through T3 Code.

ashx-j and others added 3 commits November 9, 2026 02:11
…t the radius, status colors and sidebar artwork

Published environment themes arrived only with the server config, so the app and the boot screen painted T3's standard theme first, and new desktop windows opened plain black or white.

The page now saves the last published palettes in localStorage for the runtime and the `index.html` boot script, and sends Electron the canvas it paints, which new windows and the WSL splash open in. A published theme can also set the corner radius and the success and info colors, and keep the sidebar artwork. Importing a theme that reuses a published theme's id makes a copy instead of hiding it.

Surfaces: web and desktop (one new IPC method); mobile has its own themes. Contracts: `ipc.ts` and the theme file fields. No entry point, provider, agent or connection mode changes; the cache is per browser. Docs: the plugin author README, which comes with plugins in the next PR, documents the new keys.

Built with Claude Opus 5.5 in Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Plugins turns plugins on and off

Sharing a theme meant copying files into an environment's themes folder by hand, with no way to turn it off or ship it as one package.

The server now finds plugins, folders with a `t3-plugin.json` manifest, in its `plugins` folder and the `devPaths` in `plugins.json`. It keeps each one on or off there and forgets the switch of an id no folder has, so a folder copied in later with that id starts off. It fails a plugin that needs a newer T3, and a later folder with an id it already found, because the switch belongs to the id. It publishes the themes of plugins that are on, and the theme service reads their folders only when that list changes. Settings → Plugins shows one environment at a time over a stream of plugin states, with a switch, Reload, and a rescan on every visit. Plugins run no code yet.

Surfaces: a Settings page in web and desktop; mobile shows none. No providers or agents. Contracts: five `plugins.*` RPCs. Reverse: the switch and Reload. Connections: one stream on the existing WebSocket. Docs: internals, the user guide, and a plugin author guide in `packages/plugin-sdk/README.md`, where the SDK package arrives once plugins run code.

Built with Claude Opus 5.5 in Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h browser picks where plugin UI loads from

Themes can't restyle one part of the app, and plugin CSS aimed at T3's class names breaks with most releases.

PluginHost serves an active plugin's `styles/` at `/api/plugins/<id>/…` from the plugin's record, so a failed or stopped plugin serves nothing. Once the app is idle, a small chunk adds those sheets after T3's CSS. The main areas get `data-t3-region` names, each tested on its element. "Load plugin UI from", kept per browser, picks the environments whose styles and themes load. With no plugins, the plugin system adds about 5.7 KB of minified JS to the startup chunks, about 2.1 KB gzipped, and no new chunks. A 1.2 KB watcher loads once the app is idle.

Surfaces: Settings → Plugins in web and desktop; mobile shows none. No providers or agents. Contracts: `styles`, `fromPlugin`. Reverse: each environment turns off. Connections: file GETs sign in like `/api` reads, with a DPoP proof each over T3 Connect. Docs updated.

Built with Claude Opus 5.5 in Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ashx-j
ashx-j force-pushed the feat/plugins-core branch from 1e2a2ec to 7735e79 Compare November 9, 2026 03:58
ashx-j and others added 8 commits November 9, 2026 11:25
…or unlink plugins

Plugins could only be added by copying a folder onto the server's machine, which a remote or app.t3.codes user can't do.

Dropping a folder on Settings → Plugins uploads its files over HTTP to a single-use URL the server signs, so a large folder doesn't hold up the WebSocket. The server stages them and swaps them in under the lifecycle lock, and startup restores a version an interrupted swap left in staging. A new plugin installs turned off, so its switch is the confirm step; a replaced one keeps its switch, and a dropped folder can't replace a linked one. Each row can remove its plugin, or unlink a `devPaths` one and keep the folder. A duplicate's row removes or unlinks only that copy, named by its folder.

Surfaces: drag or Add plugin in Settings, web and desktop; mobile shows none. No providers or agents. Contracts: `createUploadUrl`, and `uninstall` with an optional `folder`. Reverse: Remove and Unlink. Connections: the signature is the credential, so uploads work on every connection. Docs updated.

Built with Claude Opus 5.5 in Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plugin could only restyle the app; it had no way to run logic on the server.

A manifest's `server` entry loads through Node's CommonJS loader and activates with a small API: RPC methods and channels that clients reach through `plugins.invoke` and `plugins.subscribe`, a logger and `onDeactivate`. Activation, calls and cleanup have deadlines. A failure guard puts a plugin's unhandled rejection on its row instead of stopping the server, a plugin that does stop it stays off after the restart, and a new version that fails to start puts the old one back. Below Node 24, where tracking plugin code slows every promise, a server half fails as incompatible. The `@t3tools/plugin-sdk` package starts here, with the types a server half is written against and an `api.version` a plugin checks before using a newer API member.

Surfaces: failures show in Settings, web and desktop; mobile shows none. No providers; agent tools come later. Contracts: two RPCs, three failure stages. Reverse: turning off or reloading releases what a plugin registered. Connections: WebSocket only. Docs updated.

Built with Claude Opus 5.5 in Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t store

Plugins had no place to keep configuration or data between restarts.

A manifest can declare a `settingsSchema`, which discovery and install check, so a schema T3 can't validate fails the plugin with the reason instead of refusing every save. Clients read and save settings with `plugins.settings.get` and `.update`, and every save is checked against the schema. Top-level `t3-secret` fields go to the server's secret store, all of a plugin's in one `plugin.<id>` entry, and read back redacted. Plugins get `api.settings`, which reads from memory after the first call, and a key-value `api.storage` in `plugins.sqlite`. Every API promise, iterator pulls included, rejects with a handled `PluginApiError`. Removing a plugin deletes its settings, secrets and storage, and nothing of another plugin's or of T3's own.

Surfaces: the remove popup says what it deletes; the form comes next. Web and desktop; mobile shows none. No providers or agents. Contracts: two settings RPCs and `settingsSchema`. Reverse: removal deletes the data. Connections: WebSocket only. Docs updated.

Built with Claude Opus 5.5 in Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Schema

Plugins could declare a `settingsSchema`, but nothing in the app could show or change their settings.

A plugin row with settings now has a settings button that opens a form drawn from the schema. Each field saves on its own, secret fields go to the secret store and never show again, and a schema the form doesn't cover gets a JSON box. Rows say only whether a plugin has settings, and `plugins.settings.get` sends the schema with the settings, so a large schema doesn't ride every row update to every client. The form code sits in apps/web beside the page, its only user.

Surfaces: Settings → Plugins in web and desktop; mobile shows no plugin settings. No providers or agents. Contracts: `hasSettings` on rows and the schema in `plugins.settings.get`. Reverse: a secret's clear button removes it, and an emptied field goes back to the default. Connections: WebSocket only. Docs: internals, the user guide and the SDK README's `settingsSchema` bullet.

Built with Claude Opus 5.5 in Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plugins had no way to give agents new abilities.

A server half now registers tools with `api.mcp.registerTool`, and T3's MCP server offers them as `plg_<pluginId>_<name>`, checking `read` and `write` access like its own tools. MCP servers can't remove a tool, so a stopped plugin's tools leave new listings and answer as not found. A call that hasn't answered in 5 minutes fails with an error the agent reads and aborts the handler's signal, so a hung tool can't hold the turn. Every agent sees every enabled plugin's tools, so a plugin registers at most 32, with descriptions up to 1,000 characters and input schemas up to 4,000.

Surfaces: agents, through T3's existing MCP server, so no provider adapter changes. Agents get no tools to install, enable or remove plugins: enabling one runs its code with T3's access, so it stays a user action behind settings write, with the switch as the confirm step. No client changes. Contracts: a doc line. Docs updated.

Built with Claude Opus 5.5 in Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… call their server half, and add right panels

Plugins could restyle the app and run server code, but couldn't show their own UI.

A manifest can name a `web` bundle. For an active plugin with one, a lazy loader fetches it with the connection's credentials, imports it from a `blob:` URL and hands it T3's React and UI kit, so it ships neither. A web half calls its server half and adds right panels to its environment's threads, and a panel restored at startup says its plugin is loading until the web half has started. The SDK gains web, ui and Vite preset modules. With no plugins, the plugin system adds about 9.2 KB of minified JS to the startup chunks, about 3.5 KB gzipped, in 3 more chunks; a 2 KB watcher loads once idle, and the loader only when a plugin needs it.

Surfaces: right panels in web and desktop, whose CSP allows `blob:` scripts; mobile can't load code. No providers or agents. Contracts: `web`, `webRevision`, `hasWeb`. Reverse: turning off unloads. Connections: a DPoP proof per fetch over T3 Connect. Docs updated.

Built with Claude Opus 5.5 in Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssets, and the SDK gains theme tokens and a quick-notes example

A web half could only add right panels with React state, the SDK didn't say which theme tokens T3 keeps, and it had no plugin to copy from.

Web halves can now make atoms and read them with T3's own atom functions and hooks through the SDK's `atom` and `atom-react` modules, register command palette commands that users can bind to keys, replace the generated settings form with their own panel, and load files from an `assets/` folder. Commands have no thread, so only the first environment plugin UI loads from registers them. The loader test runs the `hello-web` settings panel on T3's atom functions and hooks, so an effect upgrade that breaks the SDK's atom types fails there. `THEME_TOKENS` lists the 27 CSS variables T3 defines under every theme, and a browser test fails when T3's stylesheet stops defining one. `examples/quick-notes` builds with `vp run build:examples`. With no plugins, the plugin system now adds about 11 KB of minified JS to startup, about 4.5 KB gzipped, in 5 more chunks.

Surfaces: the command palette, keybindings and Settings in web and desktop (CSP allows `blob:` fonts); mobile shows none. No providers or agents. Contracts: `plugin.<id>.<command>` keybindings. Reverse: unloading removes commands and panels. Connections: assets fetch like bundles. Docs updated.

Built with Claude Opus 5.5 in Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SDK's package is private and exports its TypeScript sources, so a plugin outside this repository can't install it.

`vp run build` in `packages/plugin-sdk` writes `dist`: JavaScript and bundled declarations for each entry, and a package.json that exports them with the SDK's description, license and repository. The build fails if a declaration imports anything but its siblings and React, or doesn't typecheck on its own. The README's Install section explains `npm pack`.

Surfaces: plugin-author tooling only, so no entry point, client, provider, agent, contract or connection mode changes. Reverse: none needed; `dist` is gitignored. Docs: the SDK README's Install section.

Built with Claude Opus 5.5 in Claude Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ashx-j
ashx-j force-pushed the feat/plugins-core branch from 7735e79 to 97f8959 Compare November 9, 2026 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant