You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A T3 Code SDK to expose the existing HTTP endpoints and WebSocket streams from T3 Codes server, so a web app can read threads, dispatch commands, and follow events live reusing the existing auth and reconnect logic.
For the UI, a new configurable surface reusing the browser surface. Point it at your web app's URL and the app renders "inside" T3 Code. Adding a custom surface is also the moment to tell someone that the app may act on their behalf. Web apps can optionally expose MCP servers for the agents in the T3 threads to respond to.
@t3tools/sdk, a typed client for the endpoints that already exist.
An Add plugin button in T3's surface picker, so a URL becomes a panel that can talk back to T3.
@t3tools/ui, the components T3 renders, packaged for local apps. Nice to have.
A plugin is a page your own app serves. T3 opens it in the browser surface it already ships, hands it a one-time code, and the page then calls the T3 server directly.
What a plugin can and cannot do
Can
Cannot
Read projects, threads, and turn state
Bring a thread into view in the T3 client
Create threads, start turns, answer approvals
Open, close, or navigate T3's own panels
Follow events live, resuming after a drop
Run code in the T3 renderer or server
Render as a panel, one instance per thread
Put UI anywhere but its own panel
Offer tools to agent sessions over MCP
React in place when you switch threads
Ask for scopes past orchestration:read
Store secrets in T3, or be shared through the repo
The first cannot row is deliberate. Asking a focused T3 client to reveal a thread needs a server-side broker, host registration, and desktop IPC, which is more machinery than the rest of this issue combined. It belongs in its own proposal.
Part 1, the SDK
A thin Promise-first client over what the server already exposes: the public descriptor for discovery, bearer or code-exchange auth, shell and thread snapshots, typed command dispatch, and subscriptions that resume with afterSequence after a dropped socket. It would need effect and a narrow public entry into @t3tools/contracts, leaving the internal RPC, settings, provider, and IPC contracts private.
This runs in the plugin page itself. No backend, no proxy. The server sends access-control-allow-origin: * with authorization in its allowed headers (apps/server/src/httpCors.ts:11), so a page on any origin can call /api/* with a bearer token today, and WS /ws sits outside CORS entirely.
import{createT3Client}from"@t3tools/sdk/promise";// T3 opened this page with #t3=<base url + one-time code>consthandoff=readT3Handoff(location.hash);constclient=createT3Client({baseUrl: handoff.serverUrl,auth: {type: "code",code: handoff.code},});constthreadId=newURLSearchParams(location.search).get("thread");constthread=awaitclient.thread(threadId);console.log(thread.title);forawait(constitemofclient.subscribeThread(threadId)){if(item.kind==="event")render(item.event);}
Writes are the same client. thread.create and thread.turn.start are the wire shapes T3's own clients send, and they carry boilerplate an integrator should never hand-roll (commandId, messageId, createdAt, the message envelope, runtimeMode, interactionMode), so the SDK should mint all of it.
constthreadId=crypto.randomUUID();awaitclient.createThread({
threadId,
projectId,title: "Launched from my plugin",
modelSelection,runtimeMode: "approval-required",interactionMode: "default",branch: null,worktreePath: null,});awaitclient.startTurn({ threadId,message: {text: "Summarize the open TODOs."}});
Failures should reject with typed classes, so a caller can tell an expired grant from a rejected command.
Part 2, adding a plugin
Add one tile to the surface picker: Add plugin. Paste an http(s) URL, T3 fetches that page once for its title, favicon, and description, and the entry is saved per project in server settings (settings.json, already a validated and watched JSON store at apps/server/src/config.ts:118). Entry names stay within [a-z0-9-], because the name becomes a tool prefix later.
Opening an entry substitutes {threadId} and {projectId}, and appends a fragment carrying the environment base URL and a one-time code. An unresolved {threadId} is left alone, so the same template also works for a project-level open. Right-panel surfaces are per thread already (rightPanelStore.ts:52), so each thread opens its own instance with its own thread id.
The page exchanges that code at POST /oauth/token, the RFC 8693 exchange the server already serves for pairing, and gets a short-lived token scoped to orchestration:read. A plugin can ask for more, and what it asked for appears on the consent screen in plain words.
The consent screen
Adding a plugin means handing a third party a token that acts as you, so T3 says so before it happens. One screen, two separate grants, both keyed to the origin:
This page may call T3 as you, listing the scopes it asked for.
Agents may use this plugin's tools, shown only when the entry declares an MCP URL.
The origin is the identity, since a page's title and favicon are self-reported and can change at any time. Those are shown as decoration and labelled as coming from the page. A grant is remembered until it is revoked, appears in t3 auth session list, and dies with t3 auth session revoke. Changing the title cannot inherit another origin's grant.
The optional MCP URL
An entry can declare an MCP URL, which T3 merges into an agent session's mcpServers beside its own t3-code entry. That wiring already exists for one server (ClaudeAdapter.ts:3551, type: "http" plus an auth header) and is resolved per thread at session start, so a registered plugin's tools are available in every thread of that project. HTTP only, and no headers, which keeps T3 out of the business of storing your plugin's secrets. A plugin that already serves a page can serve /mcp from the same process, so this asks for no extra bridge.
Tools show up to the agent as mcp__<entry-name>__<tool>, the same shape as T3's own mcp__t3-code__* tools.
Part 3, the UI components (nice to have)
Move the UI primitives T3 already has, such as button, dialog, command palette, tooltip, and the form controls, into @t3tools/ui with the theme stylesheet beside them. A plugin page then renders the same components as the app around it. T3 keeps rendering the same code, and nothing changes visually.
import{Button}from"@t3tools/ui/button";
Parts 1 and 2 stand on their own, so this one can wait or be dropped.
Compatibility and safety
Optional pieces sit behind a capability flag on the public descriptor. Absent means unsupported, and clients stay quiet.
The fragment carries a one-time code and never a long-lived token. Fragments are not sent to the plugin's own web server and stay out of its logs.
Plugin URLs resolve to http(s) only, and grants are per origin, per environment, revocable, and visible in t3 auth session list.
Nothing is injected into the page. No preload, no bridge, no postMessage API.
Questions for maintainers
Package names and which entry points are public and stable. Whether the one-time code should ride the URL fragment or wait for a real handoff channel. Whether @t3tools/ui should be shared with external apps at all, or stay internal with a documented copy path.
Related
#377 asked for a server SDK. #1582 proposed an extension system with a marketplace. #5020 proposed a Pi-style extension API. #6158 is an open local plugin implementation. #3993 was an earlier plugin attempt that closed after the orchestration foundation changed.
This discussion was converted from issue #6419 on August 15, 2026 09:53.
Heading
Bold
Italic
Quote
Code
Link
Numbered list
Unordered list
Task list
Attach files
Mention
Reference
Menu
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
T3 Code SDK
A T3 Code SDK to expose the existing HTTP endpoints and WebSocket streams from T3 Codes server, so a web app can read threads, dispatch commands, and follow events live reusing the existing auth and reconnect logic.
For the UI, a new configurable surface reusing the browser surface. Point it at your web app's URL and the app renders "inside" T3 Code. Adding a custom surface is also the moment to tell someone that the app may act on their behalf. Web apps can optionally expose MCP servers for the agents in the T3 threads to respond to.
@t3tools/sdk, a typed client for the endpoints that already exist.@t3tools/ui, the components T3 renders, packaged for local apps. Nice to have.A plugin is a page your own app serves. T3 opens it in the browser surface it already ships, hands it a one-time code, and the page then calls the T3 server directly.
What a plugin can and cannot do
orchestration:readThe first cannot row is deliberate. Asking a focused T3 client to reveal a thread needs a server-side broker, host registration, and desktop IPC, which is more machinery than the rest of this issue combined. It belongs in its own proposal.
Part 1, the SDK
A thin Promise-first client over what the server already exposes: the public descriptor for discovery, bearer or code-exchange auth, shell and thread snapshots, typed command dispatch, and subscriptions that resume with
afterSequenceafter a dropped socket. It would needeffectand a narrow public entry into@t3tools/contracts, leaving the internal RPC, settings, provider, and IPC contracts private.This runs in the plugin page itself. No backend, no proxy. The server sends
access-control-allow-origin: *withauthorizationin its allowed headers (apps/server/src/httpCors.ts:11), so a page on any origin can call/api/*with a bearer token today, andWS /wssits outside CORS entirely.Writes are the same client.
thread.createandthread.turn.startare the wire shapes T3's own clients send, and they carry boilerplate an integrator should never hand-roll (commandId,messageId,createdAt, the message envelope,runtimeMode,interactionMode), so the SDK should mint all of it.Failures should reject with typed classes, so a caller can tell an expired grant from a rejected command.
Part 2, adding a plugin
Add one tile to the surface picker: Add plugin. Paste an http(s) URL, T3 fetches that page once for its title, favicon, and description, and the entry is saved per project in server settings (
settings.json, already a validated and watched JSON store atapps/server/src/config.ts:118). Entry names stay within[a-z0-9-], because the name becomes a tool prefix later.Opening an entry substitutes
{threadId}and{projectId}, and appends a fragment carrying the environment base URL and a one-time code. An unresolved{threadId}is left alone, so the same template also works for a project-level open. Right-panel surfaces are per thread already (rightPanelStore.ts:52), so each thread opens its own instance with its own thread id.The page exchanges that code at
POST /oauth/token, the RFC 8693 exchange the server already serves for pairing, and gets a short-lived token scoped toorchestration:read. A plugin can ask for more, and what it asked for appears on the consent screen in plain words.The consent screen
Adding a plugin means handing a third party a token that acts as you, so T3 says so before it happens. One screen, two separate grants, both keyed to the origin:
The origin is the identity, since a page's title and favicon are self-reported and can change at any time. Those are shown as decoration and labelled as coming from the page. A grant is remembered until it is revoked, appears in
t3 auth session list, and dies witht3 auth session revoke. Changing the title cannot inherit another origin's grant.The optional MCP URL
An entry can declare an MCP URL, which T3 merges into an agent session's
mcpServersbeside its ownt3-codeentry. That wiring already exists for one server (ClaudeAdapter.ts:3551,type: "http"plus an auth header) and is resolved per thread at session start, so a registered plugin's tools are available in every thread of that project. HTTP only, and no headers, which keeps T3 out of the business of storing your plugin's secrets. A plugin that already serves a page can serve/mcpfrom the same process, so this asks for no extra bridge.Tools show up to the agent as
mcp__<entry-name>__<tool>, the same shape as T3's ownmcp__t3-code__*tools.Part 3, the UI components (nice to have)
Move the UI primitives T3 already has, such as button, dialog, command palette, tooltip, and the form controls, into
@t3tools/uiwith the theme stylesheet beside them. A plugin page then renders the same components as the app around it. T3 keeps rendering the same code, and nothing changes visually.Parts 1 and 2 stand on their own, so this one can wait or be dropped.
Compatibility and safety
t3 auth session list.postMessageAPI.Questions for maintainers
Package names and which entry points are public and stable. Whether the one-time code should ride the URL fragment or wait for a real handoff channel. Whether
@t3tools/uishould be shared with external apps at all, or stay internal with a documented copy path.Related
#377 asked for a server SDK. #1582 proposed an extension system with a marketplace. #5020 proposed a Pi-style extension API. #6158 is an open local plugin implementation. #3993 was an earlier plugin attempt that closed after the orchestration foundation changed.
All reactions