Repository navigation
Plugin system roadmap (epic) #3
Description
This epic is the roadmap for a plugin system in Ash's fork of T3 Code. It exists because upstream closed the community attempts (saphid's extension series pingdotgg#13819 to pingdotgg#13829, then the plugin host pingdotgg#16047) with "let us handle this". We build it here anyway, fork-first, and keep it in a shape that upstream could adopt later if they want to.
Written by Claude Fable 5.1 on 2026-10-30 against the nightly release v0.1.6-nightly.20261030.3917 (97e35bc), then revised after an adversarial review the same day. Agents working on this: read this epic, then the phase issue you are on. Update the phase issue's checklist as you land things. Keep this epic's "Status" section current.
Goal
Anything in T3 that is not physically impossible to change at runtime should be changeable by a plugin. A plugin can add or replace behaviour on the server (events, commands, MCP tools, providers, source control, text generation, notifications, HTTP routes, scheduled work) and on the web and desktop client (settings pages, panels, palette commands, keybindings, composer commands, timeline renderers, badges, banners, sidebar sections, themes and CSS). Mobile gets plugin management and declarative plugin surfaces, not arbitrary code. Settings has a Plugins section that manages plugins and hosts each plugin's own settings.
Decisions already made (do not re-litigate)
- Trusted, in-process plugins. A plugin is code the user installed on purpose. Server plugins run inside the Node server; web plugins run as real React code in the page. No worker sandbox, no capability grants beyond declared-for-display permissions in the manifest. Sandboxing can become an opt-in later and must not shape the API now.
- Fork-first. PRs target
ashx-j/t3codemain, neverpingdotgg/t3code. Every PR stays a draft until Ash has personally looked at it. Ash flips draft to ready, not the agent. - Minimal touch points. Upstream lands 16 to 230 commits a day. Plugin code lives in new directories and packages; existing files get a registry indirection or a one-line hook, not rewrites. Four files are off limits because they churn the most and have safer seams next to them:
apps/web/src/components/ChatView.tsx,apps/server/src/orchestration-v2/Orchestrator.ts,packages/contracts/src/settings.ts,apps/server/src/persistence/Migrations.ts. The phase issues name the seam to use instead. - Build on nightlies, not
mainHEAD. Every phase branches from the newestv*-nightly.*tag on upstream (git fetch origin --tags, thengit tag --sort=-creatordate | grep nightly | head -1). Rebase onto the newest nightly only when opening the PR, not mid-phase. The fork'smainis fast-forwarded to that same tag so PR diffs show only plugin work. Ash runs nightlies on Ash's machines, so this is what the result gets tested against. - Registries, not switches. Where T3 has a closed union plus a
switch(right panel kinds, settings paths, keybinding commands, provider drivers, source control drivers, MCP toolkits), the plugin system introduces a registry that built-ins register into first and plugins register into after. Built-ins keep working exactly as before. Where T3 already has a server-supplied open variant (provider slash commands, dynamic tool items, environment themes), reuse it instead of adding a registry. - Services, not handlers. Server plugin capabilities are methods on a
PluginHostservice and the facade it hands plugins. RPC handlers, HTTP routes and MCP tools stay thin, perdocs/internals/effect-services.md. - The plugin-facing API is plain JavaScript.
effectis inlined into the server bundle, so a plugin can never share the host's Effect instance.PluginApiis Promise-based withAsyncIterablestreams andAbortSignalcancellation. Effect stays insidePluginHost. The SDK package has zero runtime dependencies. Server entries are CommonJS so the packaged single-executable server can load them; see Phase 1. - Contracts first. Everything that crosses the wire is an Effect Schema in
packages/contracts. Plugin RPC is a small generic set of methods (list, setEnabled, reload, settings get/update, invoke, subscribe), not one RPC method per plugin feature. The environment descriptor advertisesplugins: trueso old clients hide the feature. - Plugin state lives beside T3's, never inside it. Settings go in
<stateDir>/plugin-data/<id>/settings.json(upstream'ssettings.jsonwriter drops unknown keys; moved out ofplugins/<id>/in Phase 1 so replacing a plugin's files cannot lose its settings, see Plugins Phase 1: server plugin host #5). Storage goes in<stateDir>/plugins.sqlite(upstream's numbered migration manifest refuses to start when a fork migration id collides). Enabled flags and dev paths go in<stateDir>/plugins.json. Nothing plugin-related touchessettings.jsonorstatev2.sqlite. - Mobile is declarative, and later. React Native release builds cannot load code at runtime and the stores forbid it. Mobile renders plugin settings from the manifest's JSON schema and shows declarative contributions. Mobile and desktop plugin UI only reach Ash's devices if he runs fork-built clients, so the mobile half of Phase 2 and all of Phase 8 wait until Ash confirms he will. (2026-10-31: Ash will run a fork-built desktop app, not a mobile one. Mobile plugin work is dropped.) Web served by the fork server is the first-class surface.
Architecture in one page
<stateDir>/plugins/<pluginId>/
t3-plugin.json manifest: id, name, version, description, engines.t3,
server?: "./dist/server.cjs", web?: "./dist/web.js",
settingsSchema?: JSON Schema, permissions?: string[] (display only),
contributes?: { ...declarative contributions }
dist/server.cjs CommonJS, module.exports = definePlugin({ activate }) from @t3tools/plugin-sdk/server
dist/web.js single-file ESM, default export from defineWebPlugin() in @t3tools/plugin-sdk/web
<stateDir>/plugin-data/<pluginId>/settings.json validated settings (server-owned, secrets in ServerSecretStore)
<stateDir>/plugins.json { enabled: {id: bool}, devPaths: string[] }
<stateDir>/plugins.sqlite plugin_storage(plugin_id, key, value_json, updated_at)
Server. apps/server/src/plugins/ holds PluginManifest (schema in contracts), pluginDiscovery (scan dir + devPaths), PluginHost (load, activate with a child Scope so deactivate releases everything, isolate failures so a broken plugin is reported in the Plugins page and never stops the server), PluginSettingsStore, PluginStorage, and PluginApi (the Promise facade). The facade is built from the real services: Orchestrator stored-event streams, ThreadManagement, ServerSettings (read-only), ServerSecretStore, Scheduler, MCP toolkit registration, source control and provider registries, the thread notification item, HTTP router, logger. Plugin state reaches clients through plugins.subscribeStates. Plugin RPC goes through plugins.invoke / plugins.subscribe, dispatched to handlers the plugin registered by name, authorized with the caller's session scopes.
Web and desktop. apps/web/src/plugins/ holds the loader and the contribution registries. The server serves each plugin's dist/web.js at /api/plugins/<id>/web.js. The web app fetches it with the existing authenticated fetch (desktop and hosted web use bearer tokens that a bare import() cannot send), then import()s a blob: URL. React, react-dom, jsx-runtime, @effect/atom-react, the components/ui exports and the SDK are shared through globalThis.__t3PluginHost; the SDK's Vite preset rewrites those imports to the globals. No import map. The desktop CSP gains blob: in script-src; the PR says so. Built-in UI registers into the same registries at module init.
Mobile. apps/mobile/src/features/plugins/ renders the Plugins settings screen and declarative contributions. No code loading. Deferred per decision 10.
SDK. packages/plugin-sdk with server, web, and shared subpath exports, types plus definePlugin and defineWebPlugin identity helpers, and a vite preset for plugin authors. Zero runtime dependencies.
Phases
Each phase is one draft PR to the fork (Phase 4 is three), built on the newest nightly tag, opened when its checklist passes. Order matters for 0 to 3; 4 onward can interleave.
- Phase 0: Foundations. Fork sync, branch, contract schemas, SDK skeleton, capability flag, internal doc. Plugins Phase 0: foundations (contracts, SDK skeleton, capability flag) #4 (draft PR feat(plugins): contract schemas, SDK skeleton and capability flag #14)
- Phase 1: Server plugin host. Discovery, load, activate, enable/disable, settings, storage, failure isolation, RPC wiring. Plugins Phase 1: server plugin host #5 (draft PR feat(plugins): server plugin host #15)
- Phase 2: Plugins settings page on web (mobile half deferred). Plugins Phase 2: Plugins settings page on web and mobile #6 (draft PR feat(plugins): manage plugins and their settings in Settings #16)
- Phase 3: Server API breadth. Events, threads, notifications, MCP tools, scheduled work, secrets, HTTP routes, text generation. Plugins Phase 3: server plugin API breadth #7 (draft PR feat(plugins): server plugin API for events, threads, tools, scheduling and send hooks #17; notifications and text generation blocked, see Plugins Phase 3: server plugin API breadth #7)
- Phase 4: Web plugin runtime and UI contribution registries, as three PRs. Plugins Phase 4: web plugin runtime and UI contribution registries #8 (draft PRs feat(plugins): web plugin loader, settings panels and right panels #18 to feat(plugins): timeline renderers, banners, code blocks and themes from plugins #20; later extension points on
feat/plugins-ui-parts, PR waits for Ash) - Phase 5: Agent-facing hooks. Turn lifecycle hooks, context injection, plugin skills and slash commands. Plugins Phase 5: agent-facing hooks and context injection #9 (draft PR feat(plugins): run-completed hook, richer send hook and agent plugin tools #21; the rest on
feat/plugins-server-hooksandfeat/plugins-server-gaps, PRs wait for Ash) - Phase 6: Provider and source-control plugins. Plugins Phase 6: provider and source-control plugins #10 (built on
feat/plugins-providers, PR waits for Ash) - Phase 7: Distribution. Install from path, git, npm tarball; updates; example plugins; user docs. Plugins Phase 7: distribution (install, update, examples, docs) #11 (draft PRs feat(plugins): install and remove plugins from Settings #22 to fix(plugins): second review fixes for installs, npm, the run hook and the loader #26; update checks on
feat/plugins-server-hooks) - Phase 8: Mobile declarative surfaces and desktop shell hooks. Plugins Phase 8: mobile declarative surfaces and desktop shell hooks #12 (desktop half on
feat/plugins-desktopandfeat/plugins-ui-parts, PR after Ash's trial; mobile dropped)
Non-negotiables added by Ash
- Performance first. T3's users notice a dropped frame, a lying spinner, or a slow thread switch. Plugin infrastructure must cost nothing when no plugin is installed: no extra WebSocket traffic, no extra renders in the chat view, no startup wait (plugins activate after the server is listening), no bundle growth beyond the loader and registries. With plugins installed, the host pays only for what the plugin registered. Every phase PR states what it measured and the numbers; the phase issues list what to measure. Read
docs/internals/performance-regressions.mdfirst. Registries read from hot paths (timeline rows, sidebar rows) are module-levelMaps with one list-level version bump on change, never a per-row store subscription. A plugin can hurt performance; the plugin system itself may not. - Prove every change with a plugin. Each feature increment, however small, gets a tiny test plugin under
packages/plugin-sdk/fixtures/that uses the new capability, and a focused test that loads it throughPluginHost(or the web loader) and asserts the effect is visible and that built-in behaviour is unchanged. No capability lands untested by a plugin that uses it. - Keep the work log. Every session appends a comment to the work log issue Plugins work log #13 when it stops, using the template there. The final PR description is assembled from that log, so write it for a reader who was not there.
Rules for agents on this work
- Read
AGENTS.mdanddocs/internals/effect-services.md,docs/internals/web-ui.md,docs/internals/overview.md,docs/internals/performance-regressions.md,docs/internals/environment-auth.mdbefore writing code. They are law here. - Heavy commands (
vp run typecheck, tests, builds) go throughagent-build. One at a time per machine. Background anything over a minute. - No repo-wide checks.
vp run check:changedbefore pushing, targetedvp test run <files>while iterating. - "Done" for a checklist item means the named test file passes and
vp run check:changedis green. Not prose. - Never start a server against
~/.t3/userdata. Never toucht3code.service. Dev state lives in the worktree's.t3. Before any "it works in the app" claim, seed it withvp run migrate-dev-db(dev server stopped). - Worktree setup (
t3.jsonrunsscripts/setup-worktree.ts) writes.t3/setup-runningand ends with.t3/setup-doneor.t3/setup-failed. Wait for it; do not runvp ion top of it. It runsvp i --frozen-lockfile, so a new workspace package'spnpm-lock.yamlchange must be committed before another worktree can set up. - On rebase, take upstream's
pnpm-lock.yamland rerunvp i. - A new package needs its own
tsconfig.jsonandtypecheckscript, an entry in theknip:checkworkspace list in the rootpackage.json, and fixtureentryglobs inknip.jsonc. - Do not change the fork repository's GitHub Actions settings; that is Ash's decision (upstream's workflows currently run on the fork).
- Focused tests for server behaviour (host lifecycle, failure isolation, settings validation, RPC auth). No static-markup UI tests.
- Comments say why, not what. Keep
docs/internals/plugins.mdto constraints and decisions, not a feature list. - PR bodies: problem, how it was fixed, measurements, the test plugin used, before/after images for UI, and the model and harness line, per
AGENTS.md. - When a phase changes something written here, edit this epic. Do not append a second account.
Status
2026-10-30: roadmap written and reviewed. Phase 0 done on thinkpad2 by Claude Opus 5.5: fork main fast-forwarded to v0.1.6-nightly.20261030.3917, draft PR #14 open (contracts, SDK skeleton, capability flag, internal doc). Phase 1 done: draft PR #15 (server plugin host, verified on a dev server and in the packaged single executable). Phase 1 hardened after an adversarial review (fixes on #15). Phase 2 done: draft PR #16 (web Plugins page; mobile half still deferred). Phase 3 done: draft PR #17 (events, threads, projects, send hooks, MCP tools, HTTP routes, scheduling, server info; notifications and text generation blocked, see #7). Phase 4a done: draft PR #18 (web plugin loader, plugin right panels and settings panels; T3 Connect loading deferred, see #8). Phase 4b done: draft PR #19 (palette commands, plugin keybindings, composer slash commands, assets, the SDK's Vite preset; context chips moved to 4c). Phase 4c mostly done: draft PR #20 (plugin themes on every client, composer banners, tool call and code block renderers, injected styles); thread statuses, sidebar sections and the items moved from 4b remain, see #8. Phase 5 partly done: draft PR #21 (run-completed hook with catch-up, richer send hook, agent tools to invoke and toggle plugins); approval answering, context chips and plugin skills remain, see #9. Phase 7 partly done: draft PR #22 (install from a folder or git, uninstall, Settings UI, agent tools, user guide); An adversarial review of #18 to #22 found 10 issues, all fixed in draft PR #23. Updates (draft PR #24) and npm installs (draft PR #25) followed, and a second review of #23 to #25 found 10 more issues, fixed in draft PR #26; update checks, the mobile dialog and the example repo remain, see #11. Phase 6 is not started: provider and source-control drivers are built once at startup, so plugin drivers need a design for late registration first. Phase 8 waits for Ash's confirmation.
2026-10-31, Ash's answers: desktop yes, mobile no. Phase 8 does the desktop shell half only, and its PR waits until Ash has used a fork-built desktop app with Ash's real data for a few days. The mobile half of Phase 2, all mobile work in Phases 7 and 8, and the mobile dialog are dropped. No separate example repo: the examples live in the fork under packages/plugin-sdk/examples/. Plugin notifications may change Orchestrator.ts (an exception to decision 3), as long as no other feature changes. Plugin notes in threads: draft PR #27, with the first example plugin, thread-digest. Desktop menu items and global shortcuts are on feat/plugins-desktop; Ash is trying a Windows build of the newest nightly plus #27 and that branch (trial/plugins-desktop) before its PR. Ash's restyled build ported to plugins on feat/plugins-surface: surface (theme plus stylesheet) and settings-pages. Ash then asked for every item on the "plugins can't yet" list that fits public T3.
2026-11-01: every plugin branch now sits on v0.1.7-nightly.20261031.3991 (draft PRs #14 to #27 force-pushed). Built since, each with an adversarial review and its fixes on the branch: feat/plugins-ui-parts (replaceable parts, plugin buttons, menu items and badges, pages, sidebar sections, @ items, desktop tray, t3code:// links, plugin UI over T3 Connect), feat/plugins-server-hooks (answering agent requests, send hooks that replace or block, server slash commands, thread and project management, text generation, skills and instructions for Claude and Codex, update checks) and feat/plugins-providers (Phase 6, see #10). Not possible in public T3: plugin code on mobile, and slash commands that answer without a turn. @ items insert text instead of chips. The Windows trial trial/plugins-desktop-2 carries all three; their PRs wait for Ash.
Prior art worth reading before Phase 1
- Upstream state as of today: feat(extensions): add extension SDK and runtime packages pingdotgg/t3code#13819 (extension SDK) was closed by its author, who proposed an internal panel registry instead (discussion Proposal: a small internal registry for right-panel surfaces, piloted by moving Diff with no visible change pingdotgg/t3code#14938). feat(server): run consented local plugins in supervised child processes pingdotgg/t3code#16047 (plugin host) was closed by t3dotgg with "Cool - let us handle this tho :)". feat(server): deliver finished runs to plugins from an acknowledged cursor pingdotgg/t3code#16048, feat(server): typed plugin settings, write-only secrets and plugin storage pingdotgg/t3code#16050, feat(server): install plugins from npm with integrity checks and staged updates pingdotgg/t3code#16054, feat(web,mobile): manage plugins in Settings pingdotgg/t3code#16055, feat(web,mobile): install and update plugins from npm in Settings pingdotgg/t3code#16057, feat(server,web,mobile): let plugins show statuses on threads pingdotgg/t3code#16061, feat(server,web,mobile): let plugins send notifications pingdotgg/t3code#16062, feat(server,web,mobile): let plugins add context to the turns you send pingdotgg/t3code#16063, docs(plugins): add example plugins and record plugin trust constraints pingdotgg/t3code#16065 are still open and all conflict with main. Read feat(server): typed plugin settings, write-only secrets and plugin storage pingdotgg/t3code#16050 (typed settings, write-only secrets, storage) and feat(web,mobile): manage plugins in Settings pingdotgg/t3code#16055 (Settings page) for shapes, not code.
- Pi's MCP bridge extension,
packages/provider-pi/src/server/mcpExtensionSource.ts: T3 already generates code that another runtime loads. apps/server/src/environmentTheme.ts: an existing watcher of a user directory under T3 home with size and count caps, streamed to clients as an opt-in onsubscribeServerConfig.apps/server/src/sourceControl/builtInDrivers.tsandpackages/source-control-core/src/server/driver.ts: the cleanest existing "list of drivers becomes a service" pattern to copy for registries.apps/server/src/mcp/McpHttpServer.tstoolkitRegistrationandMcpToolAccess.ts: how toolkits merge into the MCP layer and why raw registration is banned by lint.apps/server/src/relay/AgentAwarenessRelay.ts: an in-process subscriber to orchestration events with a type filter and a sliding buffer. Copy its shape for plugin event streams.- Discussions [Feature]: [RFC] Extension System / Marketplace for custom integrations pingdotgg/t3code#6714 and [Feature]: Pi-style extension API for lifecycle hooks, UI contributions, and settings pingdotgg/t3code#6837 on upstream for what users asked for.
2026-11-01: the whole stack is rebased onto v0.1.7-nightly.20261031.3991 and fork main matches it. feat/plugins-ui-parts adds replaceable parts (composer controls, message bodies, agent steps, diffs, the Settings/Usage frame), plugin buttons, menu items and badges, plugin pages, sidebar sections, @ items, a desktop tray, t3code://plugins links and plugin UI over T3 Connect; Surface's composer and Settings pages are now plugins too. feat/plugins-server-hooks adds approval answering, send hooks that replace or block, server slash commands, thread and project management, text generation hooks, skills and instructions for Claude and Codex, and update checks. Both passed an adversarial review after fixes. Phase 6 (provider and source-control drivers) is in progress on feat/plugins-providers. Not possible: plugin code in the mobile app. Not built: @ chips (text insertion instead), slash commands that answer without a turn, skills for agents other than Claude and Codex. A Windows trial build of all of this is on trial/plugins-desktop-2; draft PRs for the new branches wait for Ash.
2026-11-01 (afternoon): the rest of the "not built" list is built, each part reviewed adversarially with fixes on its branch. feat/plugins-ui-parts: plugin @ items are chips. feat/plugins-providers: Add provider offers plugin drivers, and the claude-agent-acp example gives the installed app a provider plugin to try. feat/plugins-server-gaps (on top of providers): slash commands that reply without starting a turn (Ash approved one more Orchestrator.ts command for it), plugin skills in the $ picker, schedule.at, worktrees.list, secrets.request, free-form textGeneration.generate, and the calling thread for plugin tools. Checked in the dev app with real Claude Haiku 5.5 turns: plugin answers to approvals, send hooks, slash commands and replies, skills and instructions, plugin tools, chips, secrets, text generation, worktrees, and a real ACP agent through a plugin. Claude reads its tool list once per session, so plugin tools reach threads started after the plugin is on. Not possible in public T3: plugin code in the mobile app, and skills or instructions for agents other than Claude and Codex. The Windows trial trial/plugins-desktop-3 carries every branch; draft PRs for the branches after #27 wait for Ash.
2026-11-02: a Claude Fable 5.1 (xhigh) review of the whole system found 18 issues; all real ones are fixed on their branches (an activation that could be torn down halfway, install copying before checking the manifest, /-prefixed messages skipping send hooks, serial web loading, racing plugins.json writes, git credentials in errors, and more). A second Fable pass over the fixes found nothing blocking; its small findings are fixed too. The agent that built Surface updated the Surface examples on feat/plugins-surface-v2, and three capabilities it needed were added (see #8). The Windows trial now installs as a separate "T3 Code Plugins" app beside T3 Code, sharing its data; only one can run at a time.
Activity