diff --git a/.agents/skills/t3-extension-development/SKILL.md b/.agents/skills/t3-extension-development/SKILL.md new file mode 100644 index 0000000000000..15ba4f12026ec --- /dev/null +++ b/.agents/skills/t3-extension-development/SKILL.md @@ -0,0 +1,24 @@ +--- +name: t3-extension-development +description: Use when creating, modifying, debugging, packaging, or testing a T3 Code extension or its custom UI, tools, commands, hooks, or settings. +--- + +# T3 extension development + +Before creating or modifying extension code, read the [complete extension developer guide](references/api.md). Use its TypeScript starter, manifest reference, panel bridge examples, Settings-page and theme guidance, packaging checks, and troubleshooting sections for the part you are changing. + +Extensions execute trusted code in an unsandboxed Node process on the owning T3 server. A mobile or remote client controls that process through the existing connection. Resolve paths, dependencies, and credentials on the server. + +Read the installed SDK types and the project's extension files before changing them. Keep `apiVersion: 1` in `t3-extension.json`. Export a default object with `activate(context)` from the manifest's `main` module. Published packages must ship executable JavaScript, their manifest, and self-contained HTML panels. Use `@t3tools/extension-sdk` for types and `defineExtension`. + +Declare commands and tools in the manifest and register exactly those ids during activation. Tool arguments and optional command arguments use JSON Schema. Return JSON values. Keep commands useful to both clients and agents. `extensions_list` returns contribution schemas; `extensions_invoke` invokes a named command or tool on its owning environment. + +Use `context.callT3` for environment-local services: `projects.list`, `threads.list`, `threads.get` with `{threadId}`, `orchestration.dispatch` with a T3 command, `settings.get`, and `settings.update`. Mutations use the same service paths as clients. The extension's filesystem, network, and subprocess access is unrestricted. + +Declare specific event names in the manifest's `events` list and use `context.onEvent` for committed project and orchestration events. Events are live notifications, not a durable job queue. Await work inside handlers, handle failures, and make external effects idempotent where needed. Use `context.onSettingsChanged` for settings changes and `context.onDispose` to release resources. Store persistent extension data in `context.storagePath`. + +For custom UI, ship a self-contained HTML document and declare a panel. T3 injects `window.t3Extension` before the panel's scripts. Call `window.t3Extension.call(commandId, input)` to run a command on the server; read `window.t3Extension.context` for invocation context. Use the injected CSS variables (`--background`, `--foreground`, `--primary`, `--border`, `--font-sans`) and `window.t3Extension.theme`; `onThemeChanged` reports live palette/light/dark changes without a reload. Read `window.t3Extension.settings`, save field patches with `updateSettings`, and subscribe with `onSettingsChanged`. Declare `settingsPages` with field keys and/or a panel id to add independent pages to the main Settings list on every client. Use responsive layouts and accessible controls. The same document runs in desktop/web frames and mobile WebViews. Never hardcode a localhost origin or run host code in the panel. + +Install an absolute server-local folder with `extensions_install`, or use Settings > Extensions. Reload after source or manifest changes, inspect the reported status and server logs, and test the panel and its command through the actual client. Verify persistence by disabling/re-enabling and restarting the test server. Test an invalid input and activation failure. Use isolated T3 state for development. + +T3 installs this skill and the linked guide automatically in its own state directory at server startup and refreshes them on later starts. `extensions_development` returns this skill's path and the guide's `referencePath`. The repository source of the guide is `docs/extensions.md`; `examples/extensions/host-notes` is an executable example. Do not replace or edit a user's existing provider skills or configuration to make an extension discoverable. diff --git a/.agents/skills/t3-extension-development/references/api.md b/.agents/skills/t3-extension-development/references/api.md new file mode 100644 index 0000000000000..bce401d65a529 --- /dev/null +++ b/.agents/skills/t3-extension-development/references/api.md @@ -0,0 +1,483 @@ +# Developing T3 Code extensions + +Extensions add host functionality and UI to T3 Code. They run on the environment's server, including when a user controls a desktop-hosted server through T3 Connect, a browser, or mobile. Each enabled extension gets an unsandboxed Node process with the server account's filesystem, network, environment variables, and subprocess access. Install only trusted code. Process separation contains crashes; it is not a security boundary. + +## Contents + +- [Try the working example](#quick-start) +- [Build a TypeScript extension](#build-a-typescript-extension) +- [Manifest and package layout](#package-layout) +- [Host SDK and T3 services](#typescript-sdk) +- [Events and hooks](#events-and-hooks) +- [Custom UI and the panel bridge](#custom-ui) +- [Main Settings pages and themes](#settings-pages-and-themes) +- [Lifecycle, storage, and management](#lifecycle-storage-and-management) +- [Automatic development-skill installation](#automatic-development-skill-installation) +- [Development and verification](#development-and-verification) +- [Troubleshooting](#troubleshooting) + +## Quick start + +Open **Settings > Extensions**, select the owning environment, choose **Local folder**, and enter the absolute server-local path to `examples/extensions/host-notes` in this checkout. Install it and open its **Host notes** panel. Save a note, refresh, and read it from another connected client. The example also contributes a command, agent tool, status item, committed-event hook, and persistent setting. + +Agents can use `extensions_install` with `{sourceType: "local", source: "/absolute/path"}`. `extensions_list` returns installed manifests, status, settings, and tool schemas. `extensions_invoke` accepts `{id, kind: "command" | "tool", name, input, context?}`. Every operation targets one environment. Agents creating or modifying extensions should load the automatically advertised `t3-extension-development` skill with `extensions_development`. + +## Build a TypeScript extension + +The following steps create a small extension with a command, agent tool, Settings page, and panel. Run these commands in your extension project. They are independent of the T3 repository's build tooling. + +```sh +mkdir t3-example +cd t3-example +npm init -y +npm install @t3tools/extension-sdk +npm install --save-dev typescript @types/node +``` + +When developing against an unreleased T3 checkout, build the SDK with `vp run --filter @t3tools/extension-sdk build`, run `npm pack` in `packages/extension-sdk`, and install the resulting tarball in your extension project instead of the registry package. Use the SDK matching the server API you are targeting. + +Set these fields in your extension's `package.json`, keeping the dependencies npm added: + +```json +{ + "name": "acme-t3-example", + "version": "1.0.0", + "private": true, + "type": "module", + "files": ["dist", "panel.html", "t3-extension.json"], + "scripts": { + "build": "tsc -p tsconfig.json" + } +} +``` + +Keep `private: true` while experimenting. Remove it deliberately when publishing your own package. The package name used by npm and the extension's manifest id serve different purposes; the examples use `acme-t3-example` and `acme.example` respectively. + +Create `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "rootDir": "src", + "outDir": "dist", + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} +``` + +Your source tree will be: + +```text +t3-example/ + package.json + tsconfig.json + t3-extension.json + panel.html + src/ + extension.ts +``` + +Use the manifest in the next section, the host module under [TypeScript SDK](#typescript-sdk) as `src/extension.ts`, and the document under [Complete panel example](#complete-panel-example) as `panel.html`. Then: + +```sh +npm run build +``` + +In **Settings > Extensions**, choose **Local folder** and install this project's absolute path on the owning T3 server. T3 reads `dist/extension.js`, activates the host, and exposes the declared contributions. Open the extension's **Example** Settings page, change Greeting, and run Say hello. The same value is available through the panel and the agent tool. + +After editing TypeScript, rebuild before choosing **reload**. After changing the manifest or panel document, reload so the server reads those files and clients receive the new process generation. Local folders are loaded from their original location; they are not copied into T3's package storage. + +## Package layout + +An extension directory contains `t3-extension.json`, an executable module, and any panel documents or dependencies. An npm extension is a normal npm package with this layout at its root. `main` and panel `entry` paths resolve against that root. Install dependencies and compile TypeScript before installing a local extension. npm installation executes normal package lifecycle scripts on the host. T3 downloads a pinned Node/npm distribution into its own state directory, verifies its SHA-256 against the checksums shipped with T3, and caches the validated installation. Host Node/npm are not prerequisites. Lifecycle scripts receive the managed Node directory on PATH. Every extension, including a local folder, runs under this managed Node runtime. Standard `process.execPath`, `child_process.fork`, and Node subprocess arguments work in packaged T3 installations. The first activation needs access to nodejs.org; npm installations also need the configured registry. Later activations reuse the runtime. T3 does not change global Node or npm configuration. + +```json +{ + "id": "acme.example", + "name": "Example", + "description": "An example extension", + "version": "1.0.0", + "apiVersion": 1, + "main": "dist/extension.js", + "commands": [ + { + "id": "greet", + "title": "Say hello", + "description": "Return a greeting", + "menu": "thread", + "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false } + } + ], + "tools": [ + { + "id": "greet", + "description": "Return this host's greeting", + "inputSchema": { "type": "object", "properties": {}, "additionalProperties": false } + } + ], + "panels": [{ "id": "main", "title": "Example", "entry": "panel.html" }], + "settingsPages": [ + { "id": "example", "title": "Example", "settings": ["greeting"], "panelId": "main" } + ], + "events": ["thread.created"], + "statusItems": [{ "id": "greeting", "text": "Greet", "command": "greet" }], + "settings": { + "greeting": { + "title": "Greeting", + "description": "Text to return", + "type": "string", + "default": "Hello" + } + } +} +``` + +Extension ids and contribution ids use lowercase letters, digits, hyphens, and dot-separated names, beginning each segment with a letter. Extension ids are unique per environment; contribution ids are unique within their kind. Command and tool ids may match. Versions use `major.minor.patch` with an optional prerelease suffix. API version 1 is the compatibility contract; incompatible versions are rejected before executing code. + +Commands have a title, description, optional JSON Schema for arguments, and an optional `thread` or `project` menu location. Tools require a description and argument schema. Arguments are validated before a handler runs. Register exactly the command and tool ids in the manifest; mismatches fail activation. Status items reference an existing command. Settings support strings, finite numbers, and booleans, with matching defaults. Unknown setting keys and values of the wrong type are rejected. + +### Manifest reference + +| Field | Required | Authoring guidance | +| --------------------- | -------- | ------------------------------------------------------------------------------------------------------- | +| `id` | Yes | Stable identity within an environment. Changing it creates a different extension and storage namespace. | +| `name`, `description` | Yes | Human-facing identity and purpose. | +| `version` | Yes | Extension version, independent of the SDK package version. | +| `apiVersion` | Yes | Use `1`; unsupported API versions fail validation. | +| `main` | Yes | Executable host module, relative to the extension root. | +| `commands` | No | User actions with optional argument schemas and contextual menu placement. | +| `tools` | No | Agent actions with JSON Schema arguments. | +| `panels` | No | Self-contained HTML documents. | +| `settings` | No | Named string, number, or boolean fields and their defaults. | +| `settingsPages` | No | Independent main Settings pages referencing fields and/or a panel. | +| `events` | No | Exact committed-event or extension-event names to receive. | +| `statusItems` | No | Text actions that invoke declared commands. | + +An omitted contribution list contributes nothing. A manifest entry alone does not implement a command or tool: activation must register a handler with the same id. Panel ids referenced by Settings pages, setting keys referenced by pages, and command ids referenced by status items must exist in the same manifest. + +## TypeScript SDK + +Use `@t3tools/extension-sdk` for `defineExtension`, `ExtensionContext`, `InvocationContext`, and JSON types. Compile extension TypeScript to ESM JavaScript for distribution. Node does not strip TypeScript in installed `node_modules` packages. + +```ts +import { defineExtension } from "@t3tools/extension-sdk"; + +export default defineExtension({ + async activate(context) { + const greet = () => ({ text: context.settings.greeting ?? "Hello" }); + context.registerCommand("greet", greet); + context.registerTool("greet", greet); + context.onEvent(async (event) => { + if (event.type === "thread.created") console.log("A thread was created"); + }); + context.onSettingsChanged(async (settings) => { + console.log("Greeting changed:", settings.greeting); + }); + context.onDispose(async () => { + // Release resources owned by this extension. + }); + }, +}); +``` + +The SDK is versioned independently from T3 Code. `EXTENSION_API_VERSION` is the manifest API version supported by this SDK. The SDK does not bundle T3 services into the extension process. `callT3` uses a method-indexed input/result map generated from T3's service contracts, so the method determines both argument and return types. Published types are standalone and require no internal T3 packages. Dates in service calls are ISO 8601 strings; service codecs translate them at the IPC boundary. + +### Context API + +| Member | Behavior | +| ------------------------------ | -------------------------------------------------------------------------------- | +| `extensionId` | Manifest id | +| `storagePath` | Persistent server-local directory owned by this extension | +| `settings` | Current resolved values, replaced before settings listeners run | +| `registerCommand(id, handler)` | Registers a declared command | +| `registerTool(id, handler)` | Registers a declared agent tool | +| `onEvent(handler)` | Listens to event types declared in the manifest; returns an unsubscribe function | +| `onSettingsChanged(handler)` | Listens to persisted settings changes; returns an unsubscribe function | +| `onDispose(handler)` | Registers cleanup, invoked in reverse registration order | +| `callT3(method, input)` | Calls an environment-local service using the host's authority | +| `emit(type, data)` | Emits an extension event for other subscribing extensions | + +Command/tool handlers receive `(input, invocationContext)` and return a JSON value or a Promise of one. Invocation context can include `threadId`, `projectId`, and `surface` (`web`, `desktop`, `mobile`, or `agent`). Do not assume context is present for commands invoked from Settings. Agent tool calls use the actual calling thread when available. Treat explicit context as selection information, not an authorization credential. + +### T3 service calls + +| Method | Input | Result | +| ------------------------ | ----------------------------- | ------------------------- | +| `projects.list` | `{}` | Project snapshot | +| `threads.list` | `{}` | Thread shell snapshot | +| `threads.get` | `{threadId}` | Thread snapshot | +| `orchestration.dispatch` | A T3 `OrchestrationV2Command` | Committed command receipt | +| `settings.get` | `{}` | Server settings | +| `settings.update` | A `ServerSettingsPatch` | Updated server settings | + +Use command ids and timestamps required by T3's command contracts. A committed command receipt does not mean provider work has finished. Observe committed events to follow completion. Service calls preserve the same orchestration and persistence paths as clients; trusted extensions also have unrestricted direct Node runtime access. + +### Example service integration + +Read snapshots through the SDK instead of inventing HTTP URLs or accessing a different environment's database: + +```ts +const projects = await context.callT3("projects.list", {}); +const shells = await context.callT3("threads.list", {}); + +if (invocation.threadId) { + const thread = await context.callT3("threads.get", { + threadId: invocation.threadId, + }); + console.log(thread.snapshotSequence); +} + +console.log(projects.projects.length, shells.threads.length); +``` + +These statements belong inside an async handler receiving `invocation`. Method names determine the input and result types. The service is always local to the extension's server; it does not implicitly forward a thread id to another environment. + +For a mutation, dispatch the canonical T3 command. For example, declare `pin-current-thread` in `commands`, then register this inside `activate`: + +```ts +context.registerCommand("pin-current-thread", async (_input, invocation) => { + if (!invocation.threadId) throw new Error("Open a thread before running this command."); + return context.callT3("orchestration.dispatch", { + type: "thread.pin", + commandId: crypto.randomUUID(), + threadId: invocation.threadId, + }); +}); +``` + +Use a fresh command id for a new action. The returned sequence identifies committed state; asynchronous provider work may continue afterward. The generated `OrchestrationCommand` union describes supported command shapes, and TypeScript rejects missing or incorrect fields. + +Handler input is JSON; the SDK does not infer a TypeScript argument interface from your manifest's JSON Schema. Narrow values in the handler or decode them with your chosen validator before using them. Return JSON-compatible data: convert dates to ISO strings and buffers to an explicit encoding; do not return class instances, streams, functions, or cyclic objects over IPC. + +### Events and hooks + +Declare specific event names in `events`, then register `onEvent`. Events contain `{type, data}`. Project events and orchestration events are delivered after commit. Consult `packages/contracts/src/applicationEvent.ts` and `orchestrationV2.ts` for payloads. Subscribe only to events you need: streaming text events can be frequent. + +Notifications are live and are not replayed after extension/server restart. Delivery is best effort if a process stops or a handler fails. They cannot veto committed commands. Keep handlers short, handle errors, and use persistent storage and idempotency for external work that must survive a restart. Hooks never execute inside the orchestrator's decision or database transaction. + +`context.emit("changed", data)` from `acme.example` delivers `{type: "extension.acme.example.changed", data}` to extensions that declare that exact event name. These events share the same FIFO delivery lane as committed events. Each running extension has a bounded backlog of 128 notifications or 2 MiB; overload drops incoming notifications and reports a diagnostic. Slow listeners cannot block other extensions or T3's event intake. Hooks start only after activation completes. Do not echo events recursively. + +## Custom UI + +Panels are self-contained HTML documents, including their scripts, styles, and other assets. Bundle external assets into the document or use absolute URLs appropriate for the client. Relative URLs resolve in the client and are not server file paths. T3 renders the document in a trusted web/desktop frame or a mobile WebView; host Node code stays on the server. + +T3 injects `window.t3Extension` before panel scripts: + +```js +const result = await window.t3Extension.call("greet", {}); +document.querySelector("#result").textContent = result.text; +``` + +`window.t3Extension.context` contains selection and surface information. The SDK's `@t3tools/extension-sdk/panel` entry provides `getPanelBridge()` and TypeScript declarations. The bridge only needs the existing environment connection, so it works over local, remote, and T3 Connect connections without additional ports or origins. Use accessible HTML and responsive layouts. Render user data with text nodes. + +### Complete panel example + +This document works with the `greet` command and `greeting` setting in the manifest above. It needs no framework or separate development server: + +```html + + +
+ + +