> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tai42.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Author a Studio plugin

> Extend the Studio shell with pages, panels, tabs, and nav entries.

A Studio plugin extends the browser shell itself — it contributes full pages, rich tool-run panels, settings tabs, and sidebar nav entries to a running Studio. It is distinct from a [server code plugin](/guides/authors/code-plugins): a code plugin runs in the Python process and adds HTTP surface, while a Studio plugin is a browser ESM bundle that the shell loads at runtime.

A deployment turns a Studio plugin on by naming its **package** in the server [manifest](/concepts/manifest)'s `studio_plugins` list. Each named package ships its plugin dist alongside a `studio-manifest.json`; the skeleton serves the assets under `/api/plugins/{name}/studio/` and injects them into the shell.

## The registration contract

A plugin bundle exports a single `register(context)` entry. The host imports the bundle, calls `register` with a `PluginContext`, and awaits it; every contribution flows through that context — there are no free registration functions.

```ts theme={null}
import type { PluginEntry } from '@tai42/studio-sdk';

export const register: PluginEntry = (context) => {
  context.registerPage({ path: 'reports', title: 'Reports', component: ReportsPage });
  context.registerNavEntry({ path: 'reports', title: 'Reports', icon: ReportsIcon });
  context.registerToolPanel({ toolName: 'run_report', component: ReportPanel });
  context.registerSettingsTab({ id: 'reports', title: 'Reports', component: ReportsSettings });
};
```

The four methods are:

* **`registerPage`** — a full page mounted under `/plugins/{pluginId}/{path}`.
* **`registerToolPanel`** — a rich run panel for a tool by name, replacing the auto-generated form.
* **`registerSettingsTab`** — a tab in the settings area.
* **`registerNavEntry`** — a sidebar link to one of *this plugin's* pages.

The context is **sealed** once `register` settles: a registration made afterward (from a deferred timer or a post-resolve microtask) throws instead of silently dropping, and a `register` that throws commits nothing. The host stamps each contribution with the plugin's identity, so a plugin never supplies its own id and two plugins may register the same `path` without colliding.

## Nav entries

`registerNavEntry` adds a sidebar link, and its `path` must match a page the same plugin also registers — a nav entry with no backing page is a dead link and is **rejected loudly**. The optional `icon` is a component rendering a square inline SVG that fills its box and draws with `currentColor`; the host constrains it to a fixed 1em box, `aria-hidden` (the title is the accessible name). The icon's bytes live inside the bundle, so there is no external fetch and no CSP change. Omit it for a text-only entry, exactly like the core nav.

The same set is declared statically in the plugin's `studio-manifest.json` under `contributions.nav_entries`, so the server-side catalog and read surfaces can report a nav-contributing plugin without executing its bundle. That manifest field is the declarative parity of the runtime `registerNavEntry`, and it carries the same rule: every `nav_entries` entry must appear in the plugin's `pages`, or manifest load fails loudly.

## The plugin manifest

Every plugin ships a `studio-manifest.json` next to its dist. It is parsed strictly — an unknown field is a loud rejection — and carries:

* **`name`**, **`version`** — the plugin's package identity.
* **`api_version`** — the Studio-plugin API version the bundle targets (see below).
* **`entry`** — the ESM bundle filename the host imports.
* **`integrity`** — a map of every emitted asset filename (the entry bundle and every lazy chunk, plus any stylesheet) to its `sha384-<base64>` subresource-integrity hash. The host serves only integrity-listed files and injects each with its SRI hash.
* **`contributions`** — the declared `tool_panels`, `pages`, `settings_tabs`, and `nav_entries`.

## The api\_version gate

The plugin API carries a single monotonic integer, `STUDIO_PLUGIN_API_VERSION` (currently `1`), bumped only on a **breaking** change to the plugin API or to the public prop surface of a design-system component the SDK exports. The host accepts a plugin **iff** the plugin's targeted `api_version` exactly equals the host's; anything else is rejected with a "must be rebuilt" error card, and the plugin commits nothing.

Equality is the correct gate because additive changes never bump the version — a host that gains a new optional capability keeps the same version, so every existing plugin keeps loading. The one asymmetry the equality gate leaves is a plugin that *uses* a newer capability on an *older* host that lacks it; that fails loudly by construction (calling a context method the host does not define throws during `register`, caught as that plugin's error card), never through silent negotiation.

## Styling: two sanctioned paths

A plugin styles itself in exactly one of two sanctioned ways, and no other:

1. **SDK components plus inline styles** that read the design-system tokens (`var(--tai-*)`). Raw Tailwind utilities are host-internal and are not part of this contract.
2. **A plugin-shipped scoped stylesheet**, listed as a `.css` asset in the manifest `integrity` map and host-injected as an SRI'd `<link rel="stylesheet">` before the bundle's JS is imported.

A scoped stylesheet is bound by three hard rules:

* **No global resets or preflight** — never style `html`, `body`, `:root`, `*`, or bare element selectors at the top level.
* **Scope every selector** under a plugin root class the plugin renders itself, prefixed with the plugin's own package name.
* **Theme through the tokens** — colors in plugin-authored rules come from the SDK custom properties (`var(--tai-*)`), never hardcoded, so the stylesheet themes itself with no logic of its own. Bundled third-party base CSS is exempt from this rule, but the no-global and scoping rules still bind it.

<Note>
  Stylesheet injection is a **host capability**. A plugin shipping a `.css` asset to a host that does not inject stylesheets is served but never injected — the page renders with SDK-component styling only. This is the single quiet degradation the mechanism admits, so pair a Studio host with plugins built for it.
</Note>

## See also

* [Plugin API reference](/reference/studio-sdk/plugin-api) — the `PluginContext`, contribution types, and the version gate, generated from the SDK.
* [The manifest](/concepts/manifest) — the `studio_plugins` field that turns a plugin on.
* [The screens](/studio/screens) — the shell surfaces a plugin's pages and nav entries join.
* [Studio SDK reference](/reference/studio-sdk/index) — the hooks and shared components a plugin builds from.
