Skip to content

feat(plugins): palette, keybinding and composer commands from plugins - #19

Draft
ashx-j wants to merge 9 commits into
mainfrom
feat/plugins-phase-4b
Draft

ashx-j wants to merge 9 commits into
mainfrom
feat/plugins-phase-4b

Conversation

@ashx-j

@ashx-j ashx-j commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Phase 4b of the plugin system roadmap (#3, phase issue #8). It builds on #18, so until #14 to #18 merge this diff also shows their commits.

After 4a a web plugin could add panels, but users could only reach them by clicking. This lets a plugin add commands people can run from the keyboard, and gives plugin authors a build setup that uses T3's own React.

  • Commands. api.commands.register({ id, title, keywords, run }) adds a command to the command palette, grouped with the other actions. Users can bind it to a key as plugin.<pluginId>.<commandId> in keybindings.json or Settings → Keybindings, where plugin commands get their own group.
  • Slash commands. api.composer.registerSlashCommand({ name, description, run }) adds /name to the composer's / menu. Picking it removes the typed /name and runs the plugin with the thread id. Nothing is sent to the agent.
  • Navigation. api.navigate("/settings/plugins") opens a path in the app.
  • Assets. The server serves a plugin's assets/ folder at /api/plugins/<id>/assets/*, from the real folder only. api.assetUrl("icon.svg") fetches a file with the connection's credentials and returns an object URL that is revoked when the plugin unloads. Plugin HTTP routes can now end in /* to answer every path below them; the longest match wins.
  • Vite preset. @t3tools/plugin-sdk/vite exports t3PluginWeb(). It builds web.tsx into the single-file web.js T3 loads, with React, react-dom, the JSX runtime, the shared atom hooks and @t3tools/plugin-sdk/ui read from T3 at runtime. Only the names a plugin uses end up in its bundle. @t3tools/plugin-sdk/ui types T3's shared UI components.
  • hello-web is now TSX built with the preset. Its committed web.js is the build output.

Changes from the roadmap (issue #8 is updated):

  • Plugin slash commands are a new plugin-slash-command composer item, not the provider slash command variant. That variant inserts /name for the provider to read, while a plugin command runs plugin code in the client.
  • Not built yet, moved to 4c:
    • Composer context chips. The server send hook from Phase 3 already lets a plugin add context to a message. A client chip would need a send-path change for each chip kind.
    • The settings and state atoms in the web API. A plugin's web half can read its settings through its server half today.
    • commands.run for built-in commands. The shortcut dispatcher only knows keyboard handlers, so there is no single way to run a built-in command by id yet.
  • Plugin commands have no default key. Users bind them.

Measurements:

Bundle, from production web builds of #18's head and this branch, compared chunk by chunk:

  • main grew 2.4 KB. The command palette is in the startup chunk and now reads the plugin registry, so the registry (1.2 KB, before this a separate chunk that loaded with the right panel) and the puzzle icon (0.6 KB) moved into main. The rest is the palette and composer entries.

  • pluginLoader grew 1.2 KB for commands, slash commands, navigation and assets. It still downloads only when an active plugin has a web part.

  • _chat grew 0.5 KB and the Keybindings page 0.3 KB.

  • themePalette shrank 19.8 KB and a server chunk grew by the same amount, which is chunk reshuffling with no net change.

  • Renders: not measured. The palette, the composer menu and Keybindings settings each gained one atom read of a plugin registry, which changes only when a plugin registers or unregisters something. No timeline or sidebar code changed.

  • Loader: unchanged from feat(plugins): web plugin loader, settings panels and right panels #18. With no active web plugin the loader chunk still never downloads.

  • Keyboard: all plugin shortcuts share one dispatcher scope, so a key press checks one extra scope no matter how many commands plugins register.

Screenshots:

GitHub's CLI can't upload images, so the 4 screenshots are in the T3 thread for this work instead: /hello in the composer menu, its toast with the thread id, the palette finding the plugin command, and the plugin binding under Plugins in Settings → Keybindings. They still need to be dragged into this description during review.

Test plugin: hello-web, which now also registers a palette command that opens Settings → Plugins, a /hello slash command that shows a toast with the thread id, and reads assets/hello.txt in its panel.

Verification:

  • vite.test.ts builds hello-web with the preset and checks that no node_modules code is in the bundle and React, the JSX runtime and the UI kit come from T3. Its first run caught development builds pulling in React's dev JSX runtime, which the preset now turns off.
  • pluginLoader.browser.test.ts loads the preset-built bundle, runs its command (which navigates), checks its slash command is registered, and checks both are gone after unloading.
  • keybindings.test.ts decodes a plugin.hello-web.say-hello binding and drops malformed plugin commands.
  • PluginHttpRoutes.test.ts covers /* routes and the longest match. The PluginHost.test.ts web files case serves an asset and refuses /assets/..%2Fserver.cjs.
  • In a browser against a dev server on seeded data: /hello appeared in the composer menu and showed the toast with the thread id, the palette found "Open hello-web settings" and it opened Settings → Plugins, a mod+alt+h binding to plugin.hello-web.open-settings showed under Plugins in Settings → Keybindings, and pressing Ctrl+Alt+H in a thread opened Settings → Plugins.
  • vp run check:changed passed.
  • The live panel showed the text of assets/hello.txt, fetched through api.assetUrl.
  • Not verified: the desktop app.

Review: an adversarial review of #18 to #22 found issues in this PR; the fixes are in #23.


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

ashx-j and others added 9 commits October 31, 2026 21:22
…bility flag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PluginHost discovers installed and dev plugins, loads enabled CommonJS
entries after the server is listening, and hands each a Promise-based API
for logging, settings, storage, RPC methods and channels. Failures mark the
plugin failed and never stop the server. Settings live beside T3's state
with secrets in the server secret store; storage is a lazily opened SQLite
file. Eight plugins.* WebSocket methods manage plugins, and the environment
advertises plugins: true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An adversarial review found that reloading one plugin left plugins whose
folders vanished running, that a caller disconnecting mid-disable left a
plugin half torn down but still routed, that a misspelled access level
made a write method callable by read-only sessions, and that reload kept
old code for symlinked plugin folders. Lifecycle changes now run in the
host's scope, vanished plugins are stopped, access must be read or write,
and discovery resolves real paths. Activation and cleanups get deadlines,
calls stop routing before teardown, and plugins.json keeps unknown keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings gains a Plugins page for the selected environment: each plugin
with its status, failure, enable switch and reload, plus a settings form
generated from the manifest's JSON Schema with write-only secret fields
and a JSON fallback. The page follows plugins.subscribeStates only while
open; the form model lives in client-runtime for the later mobile screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng and send hooks

Plugins can now follow stored orchestration events by type, read and act
on threads and projects, run scheduled work, see and extend outgoing
messages before they are sent, offer MCP tools to agents and serve HTTP
routes under /api/plugins/<id>, and read the server's version, descriptor
and redacted settings. The send hook runs in ThreadManagementService.dispatch
with a 2 s deadline and fails open; with no hooks it is one Ref read.

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

An adversarial review found that a send hook appending to /goal clear
turned it into setting a goal, that the SDK promised a veto nothing
honoured, that several event types merged out of sequence order, that a
schedule could outlive its plugin, and that plugin write tools skipped the
caller's mode limits. Hooks now skip slash commands, veto is gone from the
SDK, multi-type event streams stay ordered, schedules and hooks stop on
the deactivation signal, API calls end after cleanups, plugin write tools
need full access like environment writes, and tool schemas must be objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plugins could only run on the server. The server now serves each active
plugin's web bundle, and the web app loads it once an active plugin has a
web part: a starter follows plugin state after idle, and the loader fetches
bundles with the connection's credentials and imports them from blob URLs.
Bundles share the app's React, atom hooks and UI kit through a host object.

Plugins can add right panels, listed in the add menu and the empty panel,
and replace their generated settings form. An unloaded plugin's panel shows
"Plugin not active" with a close button. The desktop CSP allows blob scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Web plugins could add panels but nothing users could run from the
keyboard. Plugins can now register command palette commands that users
bind as plugin.<pluginId>.<commandId>, and slash commands in the composer
that run plugin code. They can also navigate and load files from their
assets folder, which the server serves under /api/plugins/<id>/assets/*.

The SDK gains a Vite preset that builds web.js against T3's React and UI
kit, keeping only the names a plugin uses, and typed UI components.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ashx-j
ashx-j force-pushed the feat/plugins-phase-4b branch from 7c2e744 to 645cf7e Compare October 31, 2026 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant