Repository navigation
Conversation
…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
force-pushed
the
feat/plugins-core
branch
from
November 9, 2026 03:58
1e2a2ec to
7735e79
Compare
…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
force-pushed
the
feat/plugins-core
branch
from
November 9, 2026 11:25
7735e79 to
97f8959
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#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.jsonmanifest. In this version it can:styles/CSS. The main areas have stabledata-t3-regionnames to aim CSS at.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
styles/folder, and each browser picks where plugin UI loads fromEach 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:
Quick notes as a right panel. The navy sidebar, the dark composer and its orange outline come from 3 CSS-only plugins:
Notes
vp run check:changedlocally. 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.