Skip to content

Plugin system roadmap (epic) #3

Open

Description

@ashx-j

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)

  1. 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.
  2. Fork-first. PRs target ashx-j/t3code main, never pingdotgg/t3code. Every PR stays a draft until Ash has personally looked at it. Ash flips draft to ready, not the agent.
  3. 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.
  4. Build on nightlies, not main HEAD. Every phase branches from the newest v*-nightly.* tag on upstream (git fetch origin --tags, then git tag --sort=-creatordate | grep nightly | head -1). Rebase onto the newest nightly only when opening the PR, not mid-phase. The fork's main is 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.
  5. 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.
  6. Services, not handlers. Server plugin capabilities are methods on a PluginHost service and the facade it hands plugins. RPC handlers, HTTP routes and MCP tools stay thin, per docs/internals/effect-services.md.
  7. The plugin-facing API is plain JavaScript. effect is inlined into the server bundle, so a plugin can never share the host's Effect instance. PluginApi is Promise-based with AsyncIterable streams and AbortSignal cancellation. Effect stays inside PluginHost. The SDK package has zero runtime dependencies. Server entries are CommonJS so the packaged single-executable server can load them; see Phase 1.
  8. 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 advertises plugins: true so old clients hide the feature.
  9. Plugin state lives beside T3's, never inside it. Settings go in <stateDir>/plugin-data/<id>/settings.json (upstream's settings.json writer drops unknown keys; moved out of plugins/<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 touches settings.json or statev2.sqlite.
  10. 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.

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.md first. Registries read from hot paths (timeline rows, sidebar rows) are module-level Maps 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 through PluginHost (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.md and docs/internals/effect-services.md, docs/internals/web-ui.md, docs/internals/overview.md, docs/internals/performance-regressions.md, docs/internals/environment-auth.md before writing code. They are law here.
  • Heavy commands (vp run typecheck, tests, builds) go through agent-build. One at a time per machine. Background anything over a minute.
  • No repo-wide checks. vp run check:changed before pushing, targeted vp test run <files> while iterating.
  • "Done" for a checklist item means the named test file passes and vp run check:changed is green. Not prose.
  • Never start a server against ~/.t3/userdata. Never touch t3code.service. Dev state lives in the worktree's .t3. Before any "it works in the app" claim, seed it with vp run migrate-dev-db (dev server stopped).
  • Worktree setup (t3.json runs scripts/setup-worktree.ts) writes .t3/setup-running and ends with .t3/setup-done or .t3/setup-failed. Wait for it; do not run vp i on top of it. It runs vp i --frozen-lockfile, so a new workspace package's pnpm-lock.yaml change must be committed before another worktree can set up.
  • On rebase, take upstream's pnpm-lock.yaml and rerun vp i.
  • A new package needs its own tsconfig.json and typecheck script, an entry in the knip:check workspace list in the root package.json, and fixture entry globs in knip.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.md to 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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pluginsPlugin system roadmap

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions