diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b26e7ab1f..54b6c2ec8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -48,6 +48,30 @@ jobs:
run: nr knip
continue-on-error: true
+ docs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+
+ - name: Install pnpm
+ uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b
+ with:
+ install: false
+
+ - name: Set node
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
+ with:
+ node-version: lts/*
+
+ - name: Setup
+ run: npm i -g @antfu/ni
+
+ - name: Install
+ run: nci
+
+ - name: Build docs
+ run: nr docs:build
+
test:
runs-on: ${{ matrix.os }}
strategy:
diff --git a/.github/workflows/src/ci.yml b/.github/workflows/src/ci.yml
index 6da4fb39a..b69f2e60f 100644
--- a/.github/workflows/src/ci.yml
+++ b/.github/workflows/src/ci.yml
@@ -48,6 +48,33 @@ jobs:
run: nr knip
continue-on-error: true
+ docs:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Install pnpm
+ uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b
+ with:
+ install: false
+
+ - name: Set node
+ uses: actions/setup-node@v5
+ with:
+ node-version: lts/*
+
+ - name: Setup
+ run: npm i -g @antfu/ni
+
+ - name: Install
+ run: nci
+
+ # Mirrors the Netlify build command so a broken docs build is caught in
+ # CI instead of silently leaving the published site on a stale deploy.
+ - name: Build docs
+ run: nr docs:build
+
test:
runs-on: ${{ matrix.os }}
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index ad95f26c3..a625dda0b 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -25,16 +25,14 @@ const DevToolsKitNav = [
{ text: 'Introduction', link: '/kit/' },
{ text: 'DevTools Plugin', link: '/kit/devtools-plugin' },
{ text: 'Dock System', link: '/kit/dock-system' },
+ { text: 'Create Plugin from Devframe', link: '/kit/create-plugin-from-devframe' },
{ text: 'Client Script & Context', link: '/kit/client-context' },
{ text: 'RPC', link: '/kit/rpc' },
{ text: 'Shared State', link: '/kit/shared-state' },
- { text: 'Streaming', link: '/kit/streaming' },
{ text: 'Commands', link: '/kit/commands' },
- { text: 'When Clauses', link: '/kit/when-clauses' },
{ text: 'Messages & Notifications', link: '/kit/messages' },
{ text: 'Structured Diagnostics', link: '/kit/diagnostics' },
{ text: 'Terminals & Processes', link: '/kit/terminals' },
- { text: 'Remote Client', link: '/kit/remote-client' },
{ text: 'Examples', link: '/kit/examples' },
]
@@ -125,23 +123,7 @@ export default extendConfig(withMermaid(defineConfig({
},
{
text: 'DevTools Kit',
- items: [
- { text: 'Introduction', link: '/kit/' },
- { text: 'DevTools Plugin', link: '/kit/devtools-plugin' },
- { text: 'Dock System', link: '/kit/dock-system' },
- { text: 'Client Script & Context', link: '/kit/client-context' },
- { text: 'RPC', link: '/kit/rpc' },
- { text: 'Shared State', link: '/kit/shared-state' },
- { text: 'Streaming', link: '/kit/streaming' },
- { text: 'Commands', link: '/kit/commands' },
- { text: 'When Clauses', link: '/kit/when-clauses' },
- { text: 'Messages', link: '/kit/messages' },
- { text: 'Diagnostics', link: '/kit/diagnostics' },
- { text: 'JSON Render', link: '/kit/json-render' },
- { text: 'Terminals', link: '/kit/terminals' },
- { text: 'Remote Client', link: '/kit/remote-client' },
- { text: 'Examples', link: '/kit/examples' },
- ],
+ items: DevToolsKitNav,
},
{
text: 'Error Reference',
diff --git a/docs/errors/DTK0033.md b/docs/errors/DTK0033.md
index e61151a02..7921c9081 100644
--- a/docs/errors/DTK0033.md
+++ b/docs/errors/DTK0033.md
@@ -20,7 +20,7 @@ Running the dev server against a project whose `node_modules` directory is mount
## Fix
-Ensure the project's `node_modules` directory is writable by the user running the dev server. If `node_modules` must stay read-only, use `visibility: 'normal'` (the default) in the DevTools plugin options to always show the docks instead of relying on the persisted flag.
+Ensure the project's `node_modules` directory is writable by the user running the dev server. If `node_modules` must stay read-only, use `embeddedVisibility: 'normal'` (the default) in the DevTools plugin options to always show the docks instead of relying on the persisted flag.
## Source
diff --git a/docs/guide/index.md b/docs/guide/index.md
index 14f5caa34..a2f4c1ae8 100644
--- a/docs/guide/index.md
+++ b/docs/guide/index.md
@@ -121,7 +121,7 @@ import '@vitejs/devtools/client/inject-passive'
import '@vitejs/devtools/client/inject-hidden'
```
-See [Client Script & Context](/kit/client-context#client-script-not-injected) for how injection works and the full troubleshooting checklist.
+See the [Devframe client guide](https://devfra.me/guide/client) for how injection and the browser RPC client work.
### Building with the app
diff --git a/docs/kit/client-context.md b/docs/kit/client-context.md
index 12e61bcc5..8d0ca594c 100644
--- a/docs/kit/client-context.md
+++ b/docs/kit/client-context.md
@@ -2,110 +2,39 @@
outline: deep
---
-# Client Script & Client Context
+# Client Script & Context
-In embedded mode, Vite DevTools injects a small **client script** into your app's page. The script boots the dock and publishes the **client context** — the object that every client-side surface (dock client scripts, action buttons, your own app code) uses to talk to DevTools.
+In embedded mode, Vite DevTools injects a small **client script** into your app's page. It boots the dock and publishes the **client context** — the object every client-side surface (dock action scripts, custom renderers, your own app code) uses to talk to DevTools.
## The client script
-The client script is the browser entry of Vite DevTools (published as `@vitejs/devtools/client/inject`). When it runs in the host page it:
+The client script (`@vitejs/devtools/client/inject`) connects an RPC client to the DevTools server at `/__devtools/`, builds the client context on top of it, and mounts the embedded dock. The `DevTools()` plugin injects it automatically:
-1. Connects an RPC client to the DevTools server at `/__devtools/` (WebSocket in dev mode).
-2. Builds the `DevToolsClientContext` — dock entries, panel state, commands, when-clauses — on top of that RPC client.
-3. Publishes the context to a global slot, so `getDevToolsClientContext()` can read it from anywhere in the page.
-4. Mounts the embedded dock web component into `document.body`.
+- **During `vite dev`** via `transformIndexHtml`. A production build ships the same bootstrap through [`build.withApp`](/guide/#building-with-the-app).
+- **In client environments only** — SSR builds and server code stay untouched.
+- **In top-level windows only** — inside an iframe the script exits, so a page never mounts a second dock.
-### How injection works
-
-The `DevTools()` plugin injects the script through Vite's `transformIndexHtml` hook. During `vite dev`, every HTML page served by Vite receives a module script that imports the injection virtual module, which in turn loads the client entry. The plugin picks the entry from the project's resolved visibility: `@vitejs/devtools/client/inject` (docks shown immediately) by default, `@vitejs/devtools/client/inject-passive` (docks hidden until Shift+Alt+D, then remembered) for passive mode, or `@vitejs/devtools/client/inject-hidden` (revealed per session, never remembered) for hidden mode:
-
-```mermaid
-sequenceDiagram
- participant Vite as Vite Dev Server
- participant Page as Host Page
- participant Server as DevTools Server
-
- Vite->>Page: transformIndexHtml appends import "virtual:vite-devtools-injection"
- Page->>Page: loads @vitejs/devtools/client/inject
- Page->>Server: RPC connect (/__devtools/)
- Page->>Page: publish client context, mount dock
-```
-
-Automatic injection is scoped to where the embedded client makes sense:
-
-- **Automatic HTML injection during development** — `transformIndexHtml` mounts the embedded client in the dev server. A production build can ship the same embedded bootstrap through [`build.withApp`](/guide/#building-with-the-app).
-- **Client environments only** — SSR builds and server code stay untouched.
-- **Top-level windows only** — inside an iframe (including DevTools' own iframe panels) the script logs `[VITE DEVTOOLS] Skipping in iframe` and exits, so a page never mounts a second dock.
+Which entry is injected follows the resolved `embeddedVisibility`: `inject` (shown immediately), `inject-passive` (hidden until Shift+Alt+D, then remembered), or `inject-hidden` (revealed per session). For projects without an HTML entry, import one of these entries yourself — see [Getting Started](/guide/#projects-without-an-html-entry).
## The client context
-`DevToolsClientContext` is the client-side counterpart of the [node context](./devtools-plugin): one object carrying everything a client surface needs.
-
-| Property | Description |
-|----------|-------------|
-| `rpc` | The RPC client — `call()` server functions, register [client-side functions](/kit/rpc#client-side-functions), access shared state and streaming. |
-| `clientType` | `'embedded'` (dock inside your app) or `'standalone'` (independent DevTools page). |
-| `docks` | Dock entries and selection — `entries`, `selected`, `switchEntry()`, `toggleEntry()`. |
-| `panel` | Dock panel state: position, size, drag/resize flags. |
-| `commands` | The [command palette](./commands): `register()`, `execute()`, keybindings. |
-| `when` | The [when-clause](./when-clauses) evaluation context. |
-
-### Accessing the context
-
-From anywhere in the host page, use `getDevToolsClientContext()`. It returns `undefined` until the client script finishes initializing:
+`DevToolsClientContext` is the client-side counterpart of the [node context](./devtools-plugin). Read it anywhere in the host page with `getDevToolsClientContext()`, which returns `undefined` until the script finishes initializing:
```ts
import { getDevToolsClientContext } from '@vitejs/devtools-kit/client'
const ctx = getDevToolsClientContext()
if (ctx) {
- const modules = await ctx.rpc.call('my-plugin:get-modules')
- ctx.docks.switchEntry('my-plugin')
-}
-```
-
-[Dock client scripts](/kit/dock-system#client-script) — action buttons and custom renderers — receive the context directly as their argument, extended with two dock-scoped extras: `current` (this entry's state, DOM elements, and events) and `messages` (a [messages client](./messages) scoped to the entry):
-
-```ts
-import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'
-
-export default function setup(ctx: DockClientScriptContext) {
- ctx.current.events.on('entry:activated', async () => {
- const data = await ctx.rpc.call('my-plugin:get-modules')
- ctx.messages.info(`Loaded ${data.length} modules`)
- })
+ const data = await ctx.rpc.call('my-plugin:get-modules')
}
```
-Iframe panels run in their own document, so they create their own RPC client with [`getDevToolsRpcClient()`](/kit/rpc#in-iframe-pages) instead — the connection details are discovered automatically from the parent window.
-
-## Troubleshooting
-
-### Client script not injected
-
-Symptoms: the dock never appears, `getDevToolsClientContext()` always returns `undefined`, and the browser console has no `[VITE DEVTOOLS] Client injected` log.
-
-Injection rides on Vite's `transformIndexHtml` hook, so it requires an HTML page that Vite itself serves and transforms. Setups where the HTML comes from elsewhere skip it:
-
-- **Backend integration** — Rails, Laravel, Django, or any server rendering its own HTML while Vite only serves assets.
-- **Middleware mode** — an app framework embedding Vite's dev server without serving `index.html` through it.
-- **JS-only entries** — projects whose entry point is a script rather than an HTML file.
-
-The fix is to import the client injector manually from a browser entry (`main.ts`, `entry.client.ts`):
-
-```ts
-import '@vitejs/devtools/client/inject'
-```
-
-Keep the import out of server-only and shared SSR files, and use it only when HTML injection doesn't happen — combining both mounts the client twice. To keep the client out of production bundles, guard it as a dev-only dynamic import:
-
-```ts
-if (import.meta.env.DEV)
- import('@vitejs/devtools/client/inject')
-```
-
-### Other checks
+| Property | Description |
+|----------|-------------|
+| `rpc` | The [RPC](./rpc) client — call server functions, register client functions, reach shared state |
+| `clientType` | `'embedded'` (dock inside your app) or `'standalone'` (independent DevTools page) |
+| `docks` | Dock entries and selection — `entries`, `selected`, `switchEntry()` |
+| `panel` | Dock panel state: position, size, drag/resize flags |
+| `commands` | The [command palette](./commands): `register()`, `execute()`, keybindings |
-- **Integration enabled?** Use Vite's `devtools` config, or register the `DevTools()` plugin from `@vitejs/devtools` manually.
-- **Build output?** Enable build-time collection with `devtools: { apply: 'build' }`; use [`build.withApp`](/guide/#building-with-the-app) when the generated app should include the embedded client.
-- **Dock appears but asks for authorization?** That's client trust, a separate layer from injection — see [DTK0008](/errors/DTK0008) and the `devtools.clientAuth` option.
+The browser RPC client and connection modes are documented in the [Devframe client guide](https://devfra.me/guide/client).
diff --git a/docs/kit/commands.md b/docs/kit/commands.md
index 484566aec..5088271fe 100644
--- a/docs/kit/commands.md
+++ b/docs/kit/commands.md
@@ -2,36 +2,15 @@
outline: deep
---
-# Commands & Command Palette
+# Commands
-DevTools Kit's commands system lets plugins register executable commands on the server and client. Users discover and run them through the built-in command palette, and rebind keyboard shortcuts to taste.
+Commands are executable actions plugins register on the server or client. Users discover and run them through the built-in command palette and can rebind their keyboard shortcuts. The palette merges commands from every integration.
-## Overview
+Commands are a hub concept shared across host frameworks — see the [Devframe hub docs](https://devfra.me/guide/hub) for the full model. This page shows registering them from a Vite plugin.
-```mermaid
-sequenceDiagram
- participant Plugin as Vite Plugin (Server)
- participant Client as Browser Client
- participant Palette as Command Palette
+## Server commands
- Plugin->>Plugin: ctx.commands.register({ id, handler })
- Note over Plugin: Commands synced via shared state
- Plugin-->>Client: Server commands available
-
- Client->>Client: register({ id, action })
- Note over Client: Client commands registered locally
-
- Palette->>Palette: Merge server + client commands
- Palette->>Client: User selects command
- Client->>Plugin: RPC call (server commands)
- Client->>Client: Direct action (client commands)
-```
-
-## Server-side commands
-
-### Defining commands
-
-Use `defineCommand` and register through `ctx.commands.register()`:
+Define with `defineCommand` and register in `devtools.setup`:
```ts
import { defineCommand } from '@vitejs/devtools-kit'
@@ -46,11 +25,7 @@ const clearCache = defineCommand({
await fs.rm('.cache', { recursive: true })
},
})
-```
-
-Register it in your plugin setup:
-```ts
const plugin: Plugin = {
devtools: {
setup(ctx) {
@@ -60,276 +35,20 @@ const plugin: Plugin = {
}
```
-### Command options
+Server command handlers run on the Node side; the palette bridges the selection to an RPC call.
-| Field | Type | Description |
-|-------|------|-------------|
-| `id` | `string` | **Required.** Unique namespaced ID (e.g. `my-plugin:action`) |
-| `title` | `string` | **Required.** Human-readable title shown in the palette |
-| `description` | `string` | Optional description text |
-| `icon` | `string` | Iconify icon string (e.g. `ph:trash-duotone`) |
-| `category` | `string` | Category for grouping |
-| `showInPalette` | `boolean \| 'without-children'` | Whether to show in command palette (default: `true`). `'without-children'` shows the command but doesn't flatten children into search — they're only accessible via drill-down. |
-| `when` | `string` | Conditional visibility expression (see [When Clauses](/kit/when-clauses)) |
-| `keybindings` | `DevToolsCommandKeybinding[]` | Default keyboard shortcuts |
-| `handler` | `Function` | Server-side handler. Optional if the command is a group for children. |
-| `children` | `DevToolsServerCommandInput[]` | Static sub-commands (two levels max) |
+## Client commands
-### Command handle
-
-`register()` returns a handle for live updates:
+Register client-side actions on the [client context](./client-context) — they run directly in the browser:
```ts
-const handle = ctx.commands.register({
- id: 'my-plugin:status',
- title: 'Show Status',
- handler: () => { /* ... */ },
-})
-
-// Update later
-handle.update({ title: 'Show Status (3 items)' })
+const ctx = getDevToolsClientContext()!
-// Remove
-handle.unregister()
-```
-
-## Sub-commands
-
-Commands can have static children, forming a two-level hierarchy. Selecting a parent in the palette drills into its children.
-
-```ts
ctx.commands.register({
- id: 'git',
- title: 'Git',
- icon: 'ph:git-branch-duotone',
- category: 'tools',
- // No handler — group-only parent
- children: [
- {
- id: 'git:commit',
- title: 'Commit',
- icon: 'ph:check',
- keybindings: [{ key: 'Mod+Shift+G' }],
- handler: async () => { /* ... */ },
- },
- {
- id: 'git:push',
- title: 'Push',
- handler: async () => { /* ... */ },
- },
- {
- id: 'git:pull',
- title: 'Pull',
- handler: async () => { /* ... */ },
- },
- ],
-})
-```
-
-In the palette, users see **Git** → select → drill into **Commit**, **Push**, **Pull**. Sub-commands with keybindings (like `Mod+Shift+G` above) execute directly from the shortcut without opening the palette.
-
-Each child needs a globally unique `id`; the recommended pattern is `parentId:childAction` (`git:commit`).
-
-## Keyboard shortcuts
-
-### Defining shortcuts
-
-Add default keybindings when registering a command:
-
-```ts
-ctx.commands.register({
- id: 'my-plugin:toggle-overlay',
- title: 'Toggle Overlay',
- keybindings: [
- { key: 'Mod+Shift+O' },
- ],
- handler: () => { /* ... */ },
-})
-```
-
-### Key format
-
-Use `Mod` as a platform-aware modifier — it maps to `Cmd` on macOS and `Ctrl` on other platforms.
-
-| Key string | macOS | Windows/Linux |
-|------------|-------|---------------|
-| `Mod+K` | `Cmd+K` | `Ctrl+K` |
-| `Mod+Shift+P` | `Cmd+Shift+P` | `Ctrl+Shift+P` |
-| `Alt+N` | `Option+N` | `Alt+N` |
-
-### Conditional `when` clauses
-
-Commands support a `when` expression for conditional visibility and activation:
-
-```ts
-ctx.commands.register(defineCommand({
- id: 'my-plugin:embedded-only',
- title: 'Embedded-Only Action',
- when: 'clientType == embedded',
- handler: async () => { /* ... */ },
-}))
-```
-
-When set, the command shows in the palette and triggers via its shortcut only while the expression evaluates to `true`. The grammar covers `==`, `!=`, `&&`, `||`, `!`, bare truthy, literal `true`/`false`, and namespaced keys like `vite.mode` — see [When Clauses](/kit/when-clauses) for the full reference.
-
-### User overrides
-
-Users customise shortcuts in the DevTools Settings page under **Keyboard Shortcuts**. Overrides land in shared state and persist across sessions; setting an empty array disables a shortcut.
-
-### Shortcut editor
-
-The Settings page includes an inline shortcut editor with:
-
-- **Key capture** — click the input and press any key combination.
-- **Modifier toggles** — toggle Cmd/Ctrl, Alt, Shift individually.
-- **Conflict detection** — warns when a shortcut conflicts with common browser shortcuts (`Cmd+T` → "Open new tab", `Cmd+W` → "Close tab"), with another registered command, or with a weak single-key combo without modifiers.
-
-`KNOWN_BROWSER_SHORTCUTS` is exported from `@vitejs/devtools-kit` and maps each key combination to a human-readable description.
-
-## Command palette
-
-`Mod+K` (or `Ctrl+K` on Windows/Linux) toggles the built-in palette. It offers:
-
-- **Fuzzy search** across all registered commands (including sub-commands).
-- **Keyboard navigation** — arrow keys, Enter to select, Escape to close.
-- **Drill-down** — commands with children show breadcrumb navigation.
-- **Server-command execution** — RPC with a loading indicator.
-- **Dynamic sub-menus** — client commands can return sub-items at runtime.
-
-### Embedded vs standalone
-
-In embedded mode, the palette floats over the user's app as part of the DevTools overlay. In standalone mode, it appears as a modal dialog in the standalone DevTools window.
-
-## Client-side commands
-
-Client commands register in the webcomponent context and execute directly in the browser:
-
-```ts
-// From within the DevTools client context
-context.commands.register({
- id: 'devtools:theme',
- source: 'client',
- title: 'Theme',
- icon: 'ph:palette-duotone',
- children: [
- {
- id: 'devtools:theme:light',
- source: 'client',
- title: 'Light',
- action: () => setTheme('light'),
- },
- {
- id: 'devtools:theme:dark',
- source: 'client',
- title: 'Dark',
- action: () => setTheme('dark'),
- },
- ],
+ id: 'my-plugin:toggle-grid',
+ title: 'Toggle Layout Grid',
+ handler: () => document.body.classList.toggle('debug-grid'),
})
```
-Client commands can also return dynamic sub-items:
-
-```ts
-context.commands.register({
- id: 'devtools:docs',
- source: 'client',
- title: 'Documentation',
- action: async () => {
- const docs = await fetchDocs()
- return docs.map(doc => ({
- id: `docs:${doc.slug}`,
- source: 'client' as const,
- title: doc.title,
- action: () => window.open(doc.url, '_blank'),
- }))
- },
-})
-```
-
-## Executing programmatically
-
-Code with access to the kit context can trigger a command by id:
-
-```ts
-await ctx.commands.execute('my-plugin:clear-cache')
-
-// With arguments:
-await ctx.commands.execute('my-plugin:open-file', '/src/main.ts')
-```
-
-`execute` searches both top-level commands and children, and throws when the command isn't registered or has no handler.
-
-## Listing & introspection
-
-The host exposes a `list()` method returning serializable command entries (without handlers) — useful for custom palette UIs or exporting the current command set:
-
-```ts
-const commands = ctx.commands.list()
-for (const cmd of commands) {
- console.log(cmd.id, cmd.title, cmd.keybindings)
-}
-```
-
-Subscribe to lifecycle events to react to registrations:
-
-```ts
-ctx.commands.events.on('command:registered', (cmd) => {
- console.log('new command:', cmd.id)
-})
-ctx.commands.events.on('command:unregistered', (id) => {
- console.log('removed:', id)
-})
-```
-
-## Complete example
-
-::: code-group
-
-```ts [plugin.ts]
-///
-import type { Plugin } from 'vite'
-import { defineCommand } from '@vitejs/devtools-kit'
-
-export default function myPlugin(): Plugin {
- return {
- name: 'my-plugin',
-
- devtools: {
- setup(ctx) {
- // Simple command
- ctx.commands.register(defineCommand({
- id: 'my-plugin:restart',
- title: 'Restart Dev Server',
- icon: 'ph:arrow-clockwise-duotone',
- keybindings: [{ key: 'Mod+Shift+R' }],
- handler: async () => {
- await ctx.viteServer?.restart()
- },
- }))
-
- // Command with sub-commands
- ctx.commands.register(defineCommand({
- id: 'my-plugin:cache',
- title: 'Cache',
- icon: 'ph:database-duotone',
- children: [
- {
- id: 'my-plugin:cache:clear',
- title: 'Clear Cache',
- handler: async () => { /* ... */ },
- },
- {
- id: 'my-plugin:cache:inspect',
- title: 'Inspect Cache',
- handler: async () => { /* ... */ },
- },
- ],
- }))
- },
- },
- }
-}
-```
-
-:::
+Bind a keybinding, gate visibility with a [when clause](https://devfra.me/references/when-clauses), or nest children through the same definition — see the hub docs for those fields.
diff --git a/docs/kit/create-plugin-from-devframe.md b/docs/kit/create-plugin-from-devframe.md
new file mode 100644
index 000000000..06385a77b
--- /dev/null
+++ b/docs/kit/create-plugin-from-devframe.md
@@ -0,0 +1,48 @@
+---
+outline: deep
+---
+
+# Create Plugin from Devframe
+
+A [Devframe definition](https://devfra.me/guide/devframe-definition) is a portable devtool — the same definition runs standalone, as a CLI, through a coding agent, or inside any host framework. `createPluginFromDevframe` is the bridge that mounts one as a Vite plugin inside Vite DevTools.
+
+```ts
+// vite.config.ts
+import { createPluginFromDevframe } from '@vitejs/devtools-kit/node'
+import devtool from './my-devtool'
+
+export default {
+ plugins: [createPluginFromDevframe(devtool)],
+}
+```
+
+In its `devtools.setup`, the plugin mounts the devframe's SPA, registers an iframe dock entry synthesized from the definition's `id` / `name` / `icon` / `basePath`, and runs the devframe's own `setup(ctx)`. You author the tool once with Devframe; Kit handles the Vite wiring.
+
+## Options
+
+```ts
+createPluginFromDevframe(devtool, {
+ // Vite plugin name. Defaults to `devframe:${d.id}`.
+ name: 'my-devtool',
+
+ // Mount path. Defaults to `d.basePath` or `/__${d.id}/`.
+ base: '/__my-devtool/',
+
+ // Customize the auto-synthesized iframe dock entry — category, icon
+ // override, `when` visibility, etc. `id`, `type`, and `url` stay derived.
+ dock: { category: 'app' },
+
+ // Kit-only setup, run after the devframe's own setup and after the dock
+ // entry is registered. Use it for hub features that should not live in the
+ // portable definition — commands, terminals, extra dock entries.
+ setup(ctx) {
+ ctx.commands.register({
+ id: 'my-devtool:clear-cache',
+ title: 'Clear Cache',
+ handler: () => { /* ... */ },
+ })
+ },
+})
+```
+
+Keep the portable definition free of Vite assumptions: anything that needs the hub belongs in `opts.setup`, not in the devframe's own `setup`. See [Devframe adapters](https://devfra.me/adapters) for the other ways a definition can be deployed.
diff --git a/docs/kit/devtools-plugin.md b/docs/kit/devtools-plugin.md
index 9221f3ccd..30701919b 100644
--- a/docs/kit/devtools-plugin.md
+++ b/docs/kit/devtools-plugin.md
@@ -4,9 +4,9 @@ outline: deep
# DevTools Plugin
-A DevTools plugin is a Vite plugin with one extra hook: `devtools.setup(ctx)`. The hook receives the kit-augmented context (`KitNodeContext`) — RPC, views, and the four hub subsystems Kit owns: `docks`, `terminals`, `messages`, `commands`.
+A DevTools plugin is a Vite plugin with one extra hook: `devtools.setup(ctx)`. It runs once during Vite server initialization, only when DevTools is enabled, and receives the Vite-augmented context — RPC, views, shared state, and the hub subsystems (`docks`, `terminals`, `messages`, `commands`) — plus Vite's own `viteConfig` and `viteServer`.
-This page covers the direct hook approach. To bring in a portable [Devframe](https://devfra.me/guide/) app instead, see [`createPluginFromDevframe`](https://devfra.me/guide/adapters#kit) — Kit auto-mounts the SPA, derives the iframe dock entry from `id` / `name` / `icon` / `basePath`, then runs an optional kit-only `setup` for hub features.
+To bring in a portable [Devframe](https://devfra.me/guide/) app instead of writing the hook by hand, see [Create Plugin from Devframe](./create-plugin-from-devframe).
## Installation
@@ -30,7 +30,7 @@ yarn add -D @vitejs/devtools-kit
## Basic setup
-Add the triple-slash reference to augment Vite's `Plugin` interface with the `devtools` property:
+Add the triple-slash reference to augment Vite's `Plugin` interface with the `devtools` property, then register a dock entry in `setup`:
```ts
///
@@ -39,18 +39,8 @@ import type { Plugin } from 'vite'
export default function myPlugin(): Plugin {
return {
name: 'my-plugin',
-
- // Regular Vite plugin hooks
- configResolved(config) {
- // ...
- },
-
- // DevTools setup - only called when DevTools is enabled
devtools: {
setup(ctx) {
- console.log('DevTools setup for my-plugin')
-
- // Register dock entries, RPC functions, etc.
ctx.docks.register({
id: 'my-plugin',
title: 'My Plugin',
@@ -64,55 +54,38 @@ export default function myPlugin(): Plugin {
}
```
-`devtools.setup` runs once during Vite server initialization, when DevTools is enabled.
-
-## DevTools context
-
-The `setup` function receives a `ViteDevToolsNodeContext` providing access to every DevTools API:
-
-```ts
-const plugin: Plugin = {
- devtools: {
- setup(ctx) {
- // ctx contains everything you need
- }
- }
-}
-```
+## The context
-### Available properties
+`setup` receives a `ViteDevToolsNodeContext`. The Vite-specific slots are:
| Property | Type | Description |
|----------|------|-------------|
-| `ctx.docks` | `DocksHost` | Register and manage [dock entries](./dock-system) |
-| `ctx.views` | `ViewsHost` | Host static files for your DevTools UI |
-| `ctx.rpc` | `RpcHost` | Register [RPC functions](./rpc) and broadcast to clients |
| `ctx.viteConfig` | `ResolvedConfig` | The resolved Vite configuration |
-| `ctx.viteServer` | `ViteDevServer \| undefined` | Vite dev server instance, present in dev mode |
+| `ctx.viteServer` | `ViteDevServer \| undefined` | The dev server instance, present in dev mode |
| `ctx.mode` | `'dev' \| 'build'` | Current mode |
| `ctx.cwd` | `string` | Current working directory |
| `ctx.workspaceRoot` | `string` | Workspace root directory |
+| `ctx.views` | `ViewsHost` | Host static files for your UI (`hostStatic`) |
+| `ctx.docks` | `DocksHost` | Register [dock entries](./dock-system) |
-### Example: accessing Vite config
+The context also carries the subsystems from the hub: [`ctx.rpc`](./rpc), [`ctx.rpc.sharedState`](./shared-state), [`ctx.terminals`](./terminals), [`ctx.commands`](./commands), [`ctx.messages`](./messages), and [`ctx.diagnostics`](./diagnostics).
```ts
const plugin: Plugin = {
devtools: {
setup(ctx) {
console.log('Root:', ctx.viteConfig.root)
- console.log('Mode:', ctx.mode)
-
if (ctx.viteServer) {
console.log('Dev server is running')
}
- }
- }
+ },
+ },
}
```
-## Hosting static files
+## Hosting a static UI
-For a pre-built UI (Vue/React SPA, etc.), serve it with `ctx.views.hostStatic()`:
+For a pre-built SPA (Vue/React/Svelte/etc.), serve it with `ctx.views.hostStatic()` and point an iframe dock entry at the same route. DevTools handles dev-server middleware and copies the files into the build output at build time.
```ts
import { fileURLToPath } from 'node:url'
@@ -120,15 +93,9 @@ import { fileURLToPath } from 'node:url'
const plugin: Plugin = {
devtools: {
setup(ctx) {
- // Resolve path to your built client files
- const clientPath = fileURLToPath(
- new URL('../dist/client', import.meta.url)
- )
-
- // Host at a specific route
+ const clientPath = fileURLToPath(new URL('../dist/client', import.meta.url))
ctx.views.hostStatic('/__my-plugin/', clientPath)
- // Register as a dock entry
ctx.docks.register({
id: 'my-plugin',
title: 'My Plugin',
@@ -136,16 +103,14 @@ const plugin: Plugin = {
type: 'iframe',
url: '/__my-plugin/',
})
- }
- }
+ },
+ },
}
```
-DevTools handles dev-server middleware and copies the static files into the output directory at build time.
-
## Complete example
-A plugin with a dock entry and an RPC function that exposes module data:
+A plugin that hosts a UI, exposes module data over RPC, and surfaces a dock entry:
```ts
///
@@ -154,28 +119,20 @@ import { fileURLToPath } from 'node:url'
import { defineRpcFunction } from '@vitejs/devtools-kit'
export default function myAnalyzerPlugin(): Plugin {
- const analyzedModules = new Map()
+ const analyzedModules = new Map()
return {
name: 'my-analyzer',
- // Collect data during transforms
transform(code, id) {
- analyzedModules.set(id, {
- size: code.length,
- imports: [], // Parse imports here
- })
+ analyzedModules.set(id, { size: code.length })
},
devtools: {
setup(ctx) {
- // Host the UI
- const clientPath = fileURLToPath(
- new URL('../dist/client', import.meta.url)
- )
+ const clientPath = fileURLToPath(new URL('../dist/client', import.meta.url))
ctx.views.hostStatic('/__my-analyzer/', clientPath)
- // Register dock entry
ctx.docks.register({
id: 'my-analyzer',
title: 'Module Analyzer',
@@ -184,19 +141,15 @@ export default function myAnalyzerPlugin(): Plugin {
url: '/__my-analyzer/',
})
- // Register RPC function to fetch data
ctx.rpc.register(
defineRpcFunction({
name: 'my-analyzer:get-modules',
type: 'query',
setup: () => ({
- handler: async () => {
- return Array.from(analyzedModules.entries()).map(
- ([id, data]) => ({ id, ...data })
- )
- },
+ handler: async () =>
+ Array.from(analyzedModules, ([id, data]) => ({ id, ...data })),
}),
- })
+ }),
)
},
},
@@ -204,12 +157,14 @@ export default function myAnalyzerPlugin(): Plugin {
}
```
+RPC function shapes and the client-side call API are covered in [RPC](./rpc).
+
## Debugging with the inspector
-Vite DevTools ships the official `@devframes/plugin-inspect` inspector as a built-in panel, enabled by default with `builtinDevTools`. It shows registered RPC functions, dock entries, client scripts, and DevTools-enabled plugins — handy when verifying that everything you registered actually shows up. Open the "Inspect" dock; no extra install needed.
+Vite DevTools ships the official `@devframes/plugin-inspect` inspector as a built-in panel (enabled by default with `builtinDevTools`). It shows registered RPC functions, dock entries, client scripts, and DevTools-enabled plugins — handy for verifying that what you registered actually shows up. Open the **Inspect** dock; no extra install needed.
## Next steps
-- **[Dock System](./dock-system)** — iframe panels, action buttons, custom renderers, launchers, json-render specs.
-- **[RPC](./rpc)** — bidirectional server-client communication.
-- **[Shared State](./shared-state)** — patch-synced state across every connected client.
+- **[Dock System](./dock-system)** — dock entry types and install launchers.
+- **[RPC](./rpc)** and **[Shared State](./shared-state)** — server ↔ client communication.
+- **[Create Plugin from Devframe](./create-plugin-from-devframe)** — mount a portable devframe.
diff --git a/docs/kit/diagnostics.md b/docs/kit/diagnostics.md
index 5d1d66572..686cbb218 100644
--- a/docs/kit/diagnostics.md
+++ b/docs/kit/diagnostics.md
@@ -1,35 +1,12 @@
-# Structured Diagnostics
-
-`ctx.diagnostics` is a thin layer over [`nostics`](https://github.com/vercel-labs/nostics) that lets DevTools plugins register coded errors and warnings into a shared registry without depending on `nostics` directly. Use it for author-defined coded diagnostics — errors, warnings, deprecations — that carry a stable code, a documentation URL, and a structured payload. For free-form runtime output that should appear in the DevTools UI, use [`ctx.messages`](./messages).
-
-| Surface | Purpose | Example |
-|---------|---------|---------|
-| `ctx.diagnostics` | Coded errors and warnings emitted from node-side plugin code | `MYP0001: Plugin foo not configured` |
-| [`ctx.messages`](./messages) | Free-form, user-facing notifications shown in the Messages panel | `'Audit complete — 3 issues found'` |
-
-## API shape
-
-```ts
-interface DevToolsDiagnosticsHost {
- /**
- * Proxy-backed lookup of every registered code by name. Each entry is a
- * `nostics` `DiagnosticHandle` — a callable that builds a diagnostic and
- * routes it through registered reporters; prefix with `throw` to raise.
- */
- readonly logger: Record
+---
+outline: deep
+---
- /** Register additional diagnostic definitions. */
- register: (definitions: Record) => void
+# Structured Diagnostics
- /**
- * Mirror of `nostics`'s `defineDiagnostics`, pre-wired with the host's
- * ANSI console reporter — plugins typically omit `reporters`.
- */
- defineDiagnostics: typeof defineDiagnostics
-}
-```
+`ctx.diagnostics` lets a plugin register coded errors and warnings — each with a stable code, a docs URL, and a structured payload — into a shared registry. It's a thin layer over [`nostics`](https://github.com/vercel-labs/nostics), so plugins get coded diagnostics without depending on `nostics` directly.
-The host ships pre-seeded with devframe's own `DF*` codes plus the Vite-specific `DTK*` codes registered by `@vitejs/devtools`. Call `register()` to fold your own definitions in.
+Use it for author-defined codes (`MYP0001: Plugin not configured`). For free-form runtime output shown in the UI, use [Messages](./messages). The mechanics are documented in the [Devframe diagnostics guide](https://devfra.me/guide/diagnostics).
## Register your own codes
@@ -55,91 +32,19 @@ export function MyPlugin(): PluginWithDevTools {
})
ctx.diagnostics.register(diagnostics)
-
- // Emit codes through the shared lookup:
- ctx.diagnostics.logger.MYP0002()
},
},
}
}
```
-## Code prefix conventions
-
-Codes are 4-letter prefix + 4-digit number (e.g. `MYP0001`). Pick a prefix specific to your plugin — short enough to type, distinctive enough to avoid collisions with other integrations.
-
-Prefixes used by the in-tree packages:
-
-| Prefix | Owner |
-|--------|-------|
-| `DF` | `devframe` |
-| `DTK` | `@vitejs/devtools` |
-| `RDDT` | `@vitejs/devtools-rolldown` |
-| `VDT` | `@vitejs/devtools-vite` |
-
-Each definition supports `why` (string or function returning a string) and an optional `fix` (string or function). A `docsBase` on the definition group auto-attaches a per-code URL to every emitted diagnostic.
-
-## Emit a diagnostic
-
-Each registered code is reachable as a property on `ctx.diagnostics.logger`. Every handle is a callable — invoke it to report (returns the `Diagnostic`), or prefix with `throw` to raise.
-
-```ts
-// Throw — control flow stops here
-throw ctx.diagnostics.logger.MYP0001({ name: 'foo' })
-
-// Report without throwing (default console method: `warn`)
-ctx.diagnostics.logger.MYP0002()
-
-// Override the console method per call
-ctx.diagnostics.logger.MYP0002({}, { method: 'error' })
-
-// Attach a `cause` via the params object
-ctx.diagnostics.logger.MYP0001({ name: 'foo', cause: error })
-```
-
-The callable returns a `Diagnostic` (which extends `Error`). Prefix with `throw` so TypeScript narrows the lines after as unreachable:
-
-```ts
-throw ctx.diagnostics.logger.MYP0001({ name })
-```
-
-## Typed handle reference
+## Emit
-`ctx.diagnostics.logger` is a loosely typed proxy — it covers an unbounded set of registered codes, beyond what TypeScript can narrow. For autocompletion on your plugin's specific codes, keep a reference to the typed handle returned by `defineDiagnostics()`:
+Each registered code is a callable on `ctx.diagnostics.logger`. Prefix with `throw` to raise it, or call it to report (defaults to `console.warn`):
```ts
-const myDiagnostics = ctx.diagnostics.defineDiagnostics({
- docsBase: 'https://example.com/errors',
- codes: {
- MYP0001: { why: (p: { name: string }) => `…${p.name}` },
- },
-})
-
-// Register so the shared lookup can also see it
-ctx.diagnostics.register(myDiagnostics)
-
-// Use the typed handle directly at emit sites
-myDiagnostics.MYP0001({ name: 'foo' })
+ctx.diagnostics.logger.MYP0002() // reported
+throw ctx.diagnostics.logger.MYP0001({ name: 'foo' }) // thrown
```
-Both paths share the formatter and reporter defaults set by the host (ANSI console output).
-
-## Document your codes
-
-Pair each code with a documentation page so emitted diagnostics carry a clickable URL back to a fix:
-
-```
-docs/errors/
- index.md # Table of all codes
- MYP0001.md # One page per code
- MYP0002.md
-```
-
-Each page covers the message, cause, example, and fix; see any [DTK code page](/errors/) for the canonical template. Setting `docsBase` on `defineDiagnostics({...})` auto-attaches the URL to every emitted diagnostic.
-
-## When to use what
-
-- **`ctx.diagnostics`** — coded conditions worth looking up: misconfiguration, deprecations, validation failures, internal invariants. Always docs-backed. Often `throw`-prefixed.
-- **[`ctx.messages`](./messages)** — user-facing activity surfaces in the DevTools UI: progress indicators, audit results, "URL copied" toasts. Just a message and a level.
-
-Diagnostics target tool authors and CI; messages target the human in front of the DevTools panel.
+The registry ships pre-seeded with devframe's `DF*` codes and the Vite-specific `DTK*` codes. Vite DevTools' own codes are catalogued in the [Error Reference](/errors/).
diff --git a/docs/kit/dock-system.md b/docs/kit/dock-system.md
index e742c5d5c..97c37e4ea 100644
--- a/docs/kit/dock-system.md
+++ b/docs/kit/dock-system.md
@@ -4,51 +4,31 @@ outline: deep
# Dock System
-Dock entries are how users open your DevTools integration — clickable items in the dock, similar to the macOS Dock.
+Dock entries are how users open your integration — clickable items in the DevTools dock, similar to the macOS Dock. You register them from `devtools.setup` via `ctx.docks`.
-## Entry types
+The dock is a hub concept shared across every host framework; its full entry-type reference, when-clauses, and remote-dock support live in the [Devframe hub docs](https://devfra.me/guide/hub). This page covers the parts a Vite integration reaches for most.
-Kit supports six dock entry types:
+## Entry types
-| Type | Description | Use Case |
-|------|-------------|----------|
-| `iframe` | Displays your UI in an iframe panel | Full-featured UIs, dashboards, data visualization |
-| `action` | Button that triggers client-side scripts | Inspectors, toggles, one-time actions |
-| `custom-render` | Renders directly in the user's app DOM | When you need direct DOM access or framework integration |
-| `launcher` | Actionable setup card shown in panel | Run one-time setup tasks before showing other tools |
-| `json-render` | Renders UI from a JSON spec — no client code needed | Data panels, config viewers, simple interactive tools |
-| `group` | Collapses related entries under one dock button | Bundling a framework's tools under a single button |
+| Type | Description |
+|------|-------------|
+| `iframe` | Your UI in an isolated iframe panel — the default for full UIs |
+| `action` | A button that runs a client-side script |
+| `custom-render` | Renders directly in the user's app DOM |
+| `launcher` | A setup card that runs a one-time task before showing a tool |
+| `json-render` | UI from a serializable spec — no client bundle needed |
+| `group` | Collapses related entries under one dock button |
## Iframe panels
-The default choice — host your UI in an iframe. The frame stays isolated from the user's app and works with any framework.
-
-### Basic example
-
-```ts
-ctx.docks.register({
- id: 'my-plugin',
- title: 'My Plugin',
- icon: 'https://example.com/logo.svg',
- type: 'iframe',
- url: 'https://example.com/devtools',
-})
-```
-
-### Hosting your own UI
-
-For most use cases, you build and host your own UI. DevTools serves the static files:
+The common case: host a built SPA with `ctx.views.hostStatic()` and point an iframe entry at the same route.
```ts
import { fileURLToPath } from 'node:url'
-// Path to your built SPA
const clientDist = fileURLToPath(new URL('../dist/client', import.meta.url))
-
-// Host the static files
ctx.views.hostStatic('/__my-plugin/', clientDist)
-// Register the dock entry
ctx.docks.register({
id: 'my-plugin',
title: 'My Plugin',
@@ -58,549 +38,45 @@ ctx.docks.register({
})
```
-DevTools serves the files via dev-server middleware and copies them into the build output for production.
-
-### Dock entry options
-
-```ts
-interface DockEntry {
- /** Unique identifier for this entry */
- id: string
- /** Display title shown in the dock */
- title: string
- /** Icon URL, data URI, or Iconify icon name (e.g., 'ph:house-duotone') */
- icon: string | { light: string, dark: string }
- /** Entry type */
- type: 'iframe' | 'action' | 'custom-render' | 'launcher' | 'json-render' | 'group'
- /** Id of the group this entry belongs to — see Docked groups */
- groupId?: string
- /** Member opened when a group button is activated (for type: 'group') */
- defaultChildId?: string
- /** Per-group override of in-group sub-category order (for type: 'group') — see Categories inside a group */
- categoryOrder?: Record
- /** URL to load in the iframe (for type: 'iframe') */
- url?: string
- /** Action configuration (for type: 'action') */
- action?: { importFrom: string, importName: string }
- /** Renderer configuration (for type: 'custom-render') */
- renderer?: { importFrom: string, importName: string }
- /** Launcher configuration (for type: 'launcher') */
- launcher?: {
- title: string
- onLaunch: () => Promise
- description?: string
- buttonStart?: string
- buttonLoading?: string
- /** Bound command id — the launch button, palette, and keybinding share it */
- command?: string
- /** Terminal session this launcher tracks (enables "View in Terminal") */
- terminalSessionId?: string
- /** Author-set single line of progress/status, shown inline on the card */
- digest?: string
- }
- /** JsonRenderer handle created by ctx.createJsonRenderer() (for type: 'json-render') */
- ui?: JsonRenderer
-}
-```
-
-### Icons
-
-Icons accept a URL, a data URI, or an [Iconify](https://icon-sets.iconify.design/) name. The `ph:` (Phosphor) set pairs well with DevTools UIs.
-
-```ts
-// URL to an image
-icon: 'https://example.com/logo.svg'
-
-// Data URI
-icon: 'data:image/svg+xml,...'
-
-// Iconify icon name
-icon: 'ph:chart-bar-duotone' // Phosphor Icons
-icon: 'carbon:analytics' // Carbon Icons
-icon: 'mdi:view-dashboard' // Material Design Icons
-
-// Light/dark variants
-icon: {
- light: 'https://example.com/logo-light.svg'
- dark: 'https://example.com/logo-dark.svg'
-}
-```
-
-The [File Explorer example](/kit/examples#file-explorer) is a complete iframe-dock plugin with RPC and static-build support.
-
-### Remote-hosted UIs
-
-To skip bundling a dist with your plugin, an iframe dock can point at a hosted website that connects back to the local dev server over WebSocket. See [Remote Client](./remote-client).
-
-### Shared-iframe soft navigation
-
-A multi-tab integration — say a devtool with its own Modules / Timeline / Plugins views inside one SPA — can surface each of its tabs as its own DevTools dock while all of them share **one** live iframe and switch views by client-side (soft) navigation, with no reload.
-
-Flag the iframe dock as an **anchor** with `subTabs` and give it a `frameId`:
-
-```ts
-ctx.docks.register({
- id: 'nuxt-devtools',
- type: 'iframe',
- title: 'Nuxt DevTools',
- icon: 'i-logos:nuxt-icon',
- url: 'http://localhost:3000/__nuxt_devtools__/',
- frameId: 'nuxt-devtools', // the shared iframe these docks render into
- subTabs: { protocol: 'postmessage' }, // opt into the frame-nav adapter
-})
-```
-
-When the anchor's iframe mounts, Vite DevTools attaches the hub's frame-nav adapter. It runs a versioned, origin-locked `postMessage` handshake with the embedded app, turns the tab manifest the app reports into one **member dock** per tab (id `:`), and drives the loop both ways: selecting a member soft-navigates the shared frame, and the app's own navigation moves the DevTools highlight to match. Members are first-class docks — they honor `title`, `icon`, `order`, `category`, `when`, `badge`, and grouping (`frameId` and `groupId` are independent axes).
-
-The embedded app stays decoupled: it ships a small `postMessage` nav shim and takes no hub or RPC dependency, so this works cross-origin and in static builds. When no shim answers within the handshake window, the anchor renders as a single plain iframe dock. The protocol, the member-dock data model, and the shim contract live in devframe's [shared-iframe soft-navigation design](https://github.com/devframes/devframe/blob/main/plans/shared-iframe-soft-nav.md).
-
-Set [`visibility: 'false'`](/kit/when-clauses#render-only-visibility) on the anchor when only its synthesized member tabs should have their own dock-bar buttons — the anchor keeps driving the nav loop, but its own button disappears.
-
-## Action buttons
-
-Action buttons run a client-side script when clicked. They suit:
-
-- Temporary inspector tools (DOM inspector, component picker).
-- Feature toggles.
-- One-shot actions where a button is enough.
-
-### Registration
-
-```ts
-ctx.docks.register({
- id: 'my-inspector',
- title: 'Inspector',
- icon: 'ph:cursor-duotone',
- type: 'action',
- action: {
- importFrom: 'my-plugin/devtools-action',
- importName: 'default',
- },
-})
-```
-
-### Client script
-
-The action script runs in the user's browser. It receives the [client context](/kit/client-context), extended with the dock-scoped `current` (entry state and events) and `messages`:
-
-```ts
-// src/devtools-action.ts
-import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'
-
-export default function setupAction(ctx: DockClientScriptContext) {
- let isActive = false
- let overlay: HTMLElement | null = null
-
- ctx.current.events.on('entry:activated', () => {
- isActive = true
- console.log('Inspector activated')
-
- // Create an overlay
- overlay = document.createElement('div')
- overlay.style.cssText = `
- position: fixed;
- inset: 0;
- cursor: crosshair;
- z-index: 99999;
- `
-
- overlay.addEventListener('click', (e) => {
- const target = document.elementFromPoint(e.clientX, e.clientY)
- console.log('Selected element:', target)
- })
-
- document.body.appendChild(overlay)
- })
-
- ctx.current.events.on('entry:deactivated', () => {
- isActive = false
- console.log('Inspector deactivated')
-
- // Cleanup
- overlay?.remove()
- overlay = null
- })
-}
-```
-
-### Package export
-
-Export the action script from your package:
-
-```json
-{
- "name": "my-plugin",
- "exports": {
- ".": "./dist/index.mjs",
- "./devtools-action": "./dist/devtools-action.mjs"
- }
-}
-```
-
-### Available events
-
-| Event | Description |
-|-------|-------------|
-| `entry:activated` | Fires when the user activates this dock entry |
-| `entry:deactivated` | Fires when another entry is selected or the dock is closed |
-
-For a real-world action dock, see the [A11y Checker example](/kit/examples#a11y-checker) — it runs axe-core audits and reports violations as logs.
-
-## Custom renderers
-
-Custom renderers paint directly into the DevTools panel DOM. Use them when you want direct DOM access, want to mount a framework app into the panel, or want to skip iframe isolation.
-
-### Registration
-
-```ts
-ctx.docks.register({
- id: 'my-custom-view',
- title: 'Custom View',
- icon: 'ph:code-duotone',
- type: 'custom-render',
- renderer: {
- importFrom: 'my-plugin/devtools-renderer',
- importName: 'default',
- },
-})
-```
-
-### Renderer script
-
-```ts
-// src/devtools-renderer.ts
-import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'
-
-export default function setupRenderer(ctx: DockClientScriptContext) {
- ctx.current.events.on('dom:panel:mounted', (panel) => {
- // `panel` is a DOM element you can render into
-
- // Option 1: Vanilla JS
- panel.innerHTML = `
-
-
My Custom View
-
-
- `
- panel.querySelector('#my-btn')?.addEventListener('click', () => {
- console.log('Button clicked!')
- })
-
- // Option 2: Mount a Vue app
- // import { createApp } from 'vue'
- // import App from './App.vue'
- // createApp(App).mount(panel)
-
- // Option 3: Mount a React app
- // import { createRoot } from 'react-dom/client'
- // import App from './App'
- // createRoot(panel).render()
- })
-
- ctx.current.events.on('entry:deactivated', () => {
- // Optional cleanup
- })
-}
-```
-
-### Available events
-
-| Event | Payload | Description |
-|-------|---------|-------------|
-| `dom:panel:mounted` | `HTMLElement` | Panel DOM is ready for rendering |
-| `entry:activated` | — | Entry was activated |
-| `entry:deactivated` | — | Entry was deactivated |
-
-The panel DOM is preserved across dock-entry switches, so your UI persists and the one-time setup belongs in `dom:panel:mounted`.
-
-## Launcher entries
-
-Launchers render a dedicated setup panel and run a server-side launch task. They suit integrations that need an explicit initialization step — starting a terminal task, generating artifacts, and so on.
-
-```ts
-ctx.docks.register({
- id: 'my-launcher',
- title: 'My Setup',
- icon: 'ph:rocket-launch-duotone',
- type: 'launcher',
- launcher: {
- title: 'Initialize Integration',
- description: 'Run initial setup before opening tools',
- onLaunch: async () => {
- // perform setup work here
- },
- },
-})
-```
-
-### Binding a command
-
-Point `launcher.command` at a registered command so the launch button, the command palette, and any keybinding all run one handler. `command` is the serializable launch path, so `onLaunch` is optional:
-
-```ts
-const COMMAND_ID = 'my-plugin:start'
-ctx.commands.register({ id: COMMAND_ID, title: 'Start My App', handler: start })
-
-ctx.docks.register({
- id: 'my-launcher',
- title: 'My App',
- icon: 'ph:rocket-launch-duotone',
- type: 'launcher',
- launcher: {
- title: 'Start My App',
- command: COMMAND_ID,
- },
-})
-```
-
-### Tracking a terminal session
-
-When the launch spawns a process, tie the launcher to its terminal session. Set `terminalSessionId` to surface a **View in Terminal** action (which opens the Terminals dock focused on that session), and set `digest` to a short status of the process — its progress, not its raw output:
-
-```ts
-const session = await ctx.terminals.startChildProcess(
- { command: 'vite', args: ['dev'], cwd },
- { id: 'my-app:dev', title: 'Dev Server' },
-)
-
-ctx.docks.update({
- id: 'my-launcher',
- title: 'My App',
- icon: 'ph:rocket-launch-duotone',
- type: 'launcher',
- launcher: {
- title: 'Start My App',
- status: 'loading',
- terminalSessionId: 'my-app:dev',
- digest: 'Waiting for the server…',
- },
-})
-```
-
-The **View in Terminal** action calls the hub's `hub:docks:activate` RPC (devframe 0.7.3+), which switches the host shell to the Terminals dock and focuses the tracked session — where the full output lives.
-
-`createProcessLauncher` composes all of the above (register + command binding + `prepare` + spawn + digest + session navigation) in one call. A plain **terminal launcher** stays a launcher while a long-running process runs:
-
-```ts
-import { createProcessLauncher } from '@vitejs/devtools-kit/node'
-
-createProcessLauncher({
- id: 'my-app',
- title: 'My App',
- icon: 'ph:rocket-launch-duotone',
- process: { command: 'vite', args: ['dev'], cwd: process.cwd() },
-})
-```
-
-Pass `serve.onReady` for the common **server launcher** shape — run some commands, start a server, then replace the card with an iframe embedding it. The card shows a status while `onReady` resolves the URL, then the dock swaps to the iframe:
-
-```ts
-let url: string
-
-createProcessLauncher({
- id: 'my-ui',
- title: 'My UI',
- icon: 'ph:browser-duotone',
- // Optional: run setup (e.g. install an optional dep) before spawning.
- prepare: async () => {
- /* install-on-demand */
- },
- process: async () => {
- const port = await getPort()
- url = `http://localhost:${port}/`
- return { command: 'my-ui', args: ['--port', String(port)], cwd: process.cwd() }
- },
- serve: {
- onReady: async () => {
- await waitForServer(url)
- return url
- },
- },
-})
-```
-
-The launcher tracks the spawned process for the life of the embed. When that process exits — you stop it, it crashes, or it ends on its own — the dock swaps the iframe back to an idle launcher so the embedded UI never points at a dead server. Relaunching clears the previous run's terminal session before spawning a fresh one, so the session id stays collision-free across restarts.
-
-## JSON render panels
-
-JSON render panels describe a UI as a JSON spec on the server — the client renders it from a built-in component library. This is the shortest path to a DevTools panel: server-side TypeScript only.
-
-Create a renderer handle with `ctx.createJsonRenderer()` and pass it as `ui` when registering a `json-render` dock entry:
-
-```ts
-const ui = ctx.createJsonRenderer({
- root: 'root',
- elements: {
- root: {
- type: 'Stack',
- props: { direction: 'column', gap: 12 },
- children: ['heading', 'info'],
- },
- heading: {
- type: 'Text',
- props: { text: 'Hello from JSON!', variant: 'heading' },
- },
- info: {
- type: 'KeyValueTable',
- props: {
- data: {
- Version: '1.0.0',
- Status: 'Running',
- },
- },
- },
- },
-})
-
-ctx.docks.register({
- id: 'my-panel',
- title: 'My Panel',
- icon: 'ph:chart-bar-duotone',
- type: 'json-render',
- ui,
-})
-```
-
-See [JSON Render](/kit/json-render) for the full component reference, dynamic updates, actions, state bindings, and examples.
-
-## Docked groups
-
-Collapse several related entries under one dock button. A group shows as a single button on the dock bar; activating it reveals its members in a popover, and opening a member shows that view alongside a thin sidebar for switching between siblings. This lets a framework split its features into separate, individually-pluggable entries while presenting them as one unit.
-
-Register a `group` entry, then point each member at it with `groupId`:
-
-```ts
-ctx.docks.register({
- id: 'nuxt',
- title: 'Nuxt',
- icon: 'logos:nuxt-icon',
- type: 'group',
- defaultChildId: 'nuxt:overview',
-})
-
-ctx.docks.register({
- id: 'nuxt:overview',
- title: 'Overview',
- icon: 'ph:gauge-duotone',
- type: 'iframe',
- url: '/__nuxt-overview/',
- groupId: 'nuxt',
-})
-```
-
-A group carries the usual `title`/`icon`/`category`/`defaultOrder`/`when` fields and has no view of its own. `defaultChildId` names the member opened when the group button is activated; without it, the button reveals the member popover and opens a view once a member is chosen.
-
-Pointing `defaultChildId` at a [shared-iframe anchor](#shared-iframe-soft-navigation) that is hidden with `visibility: 'false'` is the idiomatic way to boot a soft-nav frame: activating the group mounts the anchor's iframe the first time, and every later activation resurfaces the frame's current member tab so a visible dock stays highlighted rather than the anchor itself.
-
-Membership is a flat pointer, not containment: every member stays an independently-registered top-level entry. A member whose `groupId` references a group that was never registered renders as a normal top-level entry, and a group with no members stays hidden until an entry joins it. Grouping is one level deep — a group entry does not set its own `groupId`.
-
-### Categories inside a group
-
-The `category` field plays a dual role. On a top-level entry it is the outer dock-bar bucket. On a **grouped** member — one whose `groupId` resolves to a registered group — the outer bucket is the **group's** own `category`, and the member's `category` becomes an **in-group sub-category** that divides the group's popover, edge-mode sidebar, settings list, and command-palette drill-down into sections. Sub-categories order by the same category table as the outer bar and default to `default` when unset.
-
-```ts
-// The group's category ('framework') is the outer bucket for the whole group.
-ctx.docks.register({ id: 'nuxt', title: 'Nuxt', icon: 'logos:nuxt-icon', type: 'group', category: 'framework' })
-
-// Members sort into 'app' and 'advanced' SUB-categories inside the Nuxt group,
-// while the group button itself lives in 'framework' on the bar.
-ctx.docks.register({ id: 'nuxt:overview', title: 'Overview', icon: 'ph:gauge-duotone', type: 'iframe', url: '/__nuxt/overview/', groupId: 'nuxt', category: 'app' })
-ctx.docks.register({ id: 'nuxt:graph', title: 'Graph', icon: 'ph:graph-duotone', type: 'iframe', url: '/__nuxt/graph/', groupId: 'nuxt', category: 'advanced' })
-```
-
-An orphan member (its `groupId` matches no registered group) has no group to supply an outer bucket, so it falls back to its own `category`.
-
-A group can reshuffle its own sub-category order with `categoryOrder`, a `Record` that overrides `DEFAULT_CATEGORIES_ORDER` for that group's members only — every other group and the outer dock-bar order are untouched:
-
-```ts
-// 'advanced' now leads 'app' inside this group, reversing the shared default.
-ctx.docks.register({
- id: 'nuxt',
- title: 'Nuxt',
- icon: 'logos:nuxt-icon',
- type: 'group',
- category: 'framework',
- categoryOrder: { advanced: -1, app: 1 },
-})
-```
-
-A sub-category the map omits keeps its weight from the shared table.
-
-### The built-in Vite+ group
-
-Vite DevTools seeds a built-in **Vite+** group that collects Vite ecosystem integrations under one button. Join it with the exported id:
-
-```ts
-import { DEVTOOLS_VITEPLUS_GROUP_ID } from '@vitejs/devtools-kit/constants'
-
-ctx.docks.register({
- id: 'rolldown',
- title: 'Rolldown',
- icon: 'https://example.com/rolldown.svg',
- type: 'iframe',
- url: '/__devtools-rolldown/',
- groupId: DEVTOOLS_VITEPLUS_GROUP_ID,
-})
-```
-
-DevTools for Rolldown joins this group out of the box.
+Icons accept a URL, a data URI, or an [Iconify](https://icon-sets.iconify.design/) name (the `ph:` Phosphor set pairs well with DevTools UIs), or `{ light, dark }` for theme-aware icons.
-### Visibility and order
+## Groups
-From the dock settings panel, users hide or reorder members within a group independently, and hide the whole group from its row. When a group's members span several sub-categories, each sub-category reorders on its own and shows its own header.
+Set a shared `groupId` to collapse several entries under one dock button. Vite DevTools ships the `viteplus` group, which bundles the optional first-party integrations (Rolldown, Vite, Vitest, Oxc) behind a single button — pass `groupId: 'viteplus'` to place an entry there.
-Pinning an entry moves it into a dedicated **Pinned** category that leads the dock bar ahead of every other category. A top-level entry (or a whole group button) pins to the bar-level Pinned bucket; a grouped member pins to a Pinned sub-category that leads its own group, staying inside the group rather than surfacing on the bar. A pinned entry shows even when its home category is hidden, and unpinning returns it to that category in its previous position.
+## Install launchers
-## Common options
-
-Every dock type accepts these base fields:
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `id` | `string` | Unique, namespaced. |
-| `title` | `string` | Label shown in the dock. |
-| `icon` | `string \| { light, dark }` | Iconify name, URL, data URI, or light/dark pair. |
-| `category` | `'app' \| 'framework' \| 'web' \| 'advanced' \| 'default'` | Outer dock-bar bucket, or the in-group sub-category when `groupId` resolves to a group — see [Categories inside a group](#categories-inside-a-group). Defaults to `'default'`. |
-| `defaultOrder` | `number` | Orders entries within a category; lower numbers appear first. Default `0`. |
-| `when` | `string` | Visibility expression — see [When Clauses](/kit/when-clauses). |
-| `visibility` | `string` | Render-only counterpart to `when` — hides just this entry's dock-bar button, leaving it registered and reachable. See [Render-only visibility](/kit/when-clauses#render-only-visibility). |
-| `badge` | `string` | Short text badge (e.g. unread count). |
-| `groupId` | `string` | Collapse this entry under a group's button; the group's `category` becomes this entry's outer bucket — see [Docked groups](#docked-groups). |
-
-## Update
-
-`register()` returns a handle with an `update(patch)` method:
-
-```ts
-const handle = ctx.docks.register({ /* ... */ })
-
-// Live update (e.g. refresh the badge)
-handle.update({ badge: '3' })
-```
-
-## Communication with the server
-
-Action scripts and custom renderers talk to the server through [RPC](./rpc):
+Vite DevTools stays dependency-light: an optional integration that isn't installed yet appears as a `launcher` entry. Clicking it installs the package on demand (as a tracked terminal session), then prompts a dev-server restart so the real plugin can mount. `createInstallLauncher` builds that plugin for you.
```ts
-import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'
+import { createInstallLauncher } from '@vitejs/devtools-kit/node'
-export default function setup(ctx: DockClientScriptContext) {
- ctx.current.events.on('entry:activated', async () => {
- // Call a server function
- const data = await ctx.rpc.call('my-plugin:get-data')
- console.log('Data from server:', data)
- })
+export default {
+ plugins: [
+ createInstallLauncher({
+ id: 'my-integration',
+ title: 'My Integration',
+ icon: 'ph:sparkle-duotone',
+ groupId: 'viteplus',
+ install: ['@acme/vite-devtools-my-integration@^1.0.0'],
+ }),
+ ],
}
```
-Or use `getDevToolsRpcClient()` in iframe pages:
+| Option | Description |
+|--------|-------------|
+| `id` | Dock entry id — usually the same id the real integration registers, so launcher and mounted dock share a rail slot |
+| `title` | Dock title / rail tooltip |
+| `icon` | A served URL or an Iconify `collection:name` |
+| `install` | npm specs to ensure are installed; only missing ones are installed, in one call |
+| `groupId` | Dock group, e.g. `'viteplus'` |
+| `label` | Friendly name used in the launcher copy (defaults to `title`) |
+| `pkg` | Package named in the button copy (defaults to the first `install` spec's bare name) |
+| `dev` | Install as devDependencies (default `true`) |
-```ts
-import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
+The launcher installs at the workspace root — these are devtools for the whole workspace — and, on failure, surfaces [`DTK0050`](/errors/DTK0050) with the terminal output linked from the card.
-const rpc = await getDevToolsRpcClient()
-const data = await rpc.call('my-plugin:get-data')
-```
+## Talking to the server
-See [RPC](./rpc) for complete documentation on server-client communication.
+Action scripts and iframe UIs communicate with the Node side over [RPC](./rpc), and share reactive data through [shared state](./shared-state). The `action`, `custom-render`, and `json-render` entry types, along with visibility `when` clauses, are documented in the [Devframe hub docs](https://devfra.me/guide/hub).
diff --git a/docs/kit/examples.md b/docs/kit/examples.md
index abac95140..35d40fd1e 100644
--- a/docs/kit/examples.md
+++ b/docs/kit/examples.md
@@ -10,52 +10,24 @@ Reference plugins built with `@vitejs/devtools-kit`, each focused on a different
### A11y Checker
-Accessibility auditing powered by [axe-core](https://github.com/dequelabs/axe-core).
-
-**Features demonstrated:**
-
-- An `action` dock entry with a client-side script.
-- Running axe-core audits on the current page.
-- Reporting violations as DevTools logs with severity levels.
-- Using log handles to update a summary log in place.
+Accessibility auditing powered by [axe-core](https://github.com/dequelabs/axe-core): an `action` dock entry with a client-side script that runs audits on the current page and reports violations as DevTools messages, updating a summary in place via a handle.
**Source:** [`examples/plugin-a11y-checker`](https://github.com/vitejs/devtools/tree/main/examples/plugin-a11y-checker)
### File Explorer
-A file-explorer dock that lists, reads, and writes files through RPC.
-
-**Features demonstrated:**
-
-- `static`, `query`, and `action` RPC functions.
-- Custom UI panel hosted with `context.views.hostStatic(...)`.
-- An `iframe` dock entry.
-- RPC dump support for static builds.
-- Backend-mode detection (`websocket` vs `static`) on the client.
+A file-explorer dock that lists, reads, and writes files through RPC — `static`, `query`, and `action` functions behind an `iframe` dock, with a custom UI hosted via `ctx.views.hostStatic()` and backend-mode detection on the client.
**Source:** [`examples/plugin-file-explorer`](https://github.com/vitejs/devtools/tree/main/examples/plugin-file-explorer)
### Git UI
-An interactive Git panel built from server-side JSON specs — no client code.
-
-**Features demonstrated:**
-
-- The `json-render` dock type for zero-client-code panels.
-- Building a `JsonRenderSpec` from server-side data (branch, status, log).
-- Dynamic spec updates via shared state (`sharedStateKey`).
-- Button actions bridged to RPC functions (`git-ui:refresh`, `git-ui:commit`).
-- Text input with `$bindState` two-way binding and `$state` in action params.
-- Reactively updating the dock badge.
+An interactive Git panel built entirely from server-side [json-render](https://devfra.me/guide/json-render) specs — no client bundle. Shows dynamic spec updates via shared state, button actions bridged to RPC, and two-way text input binding.
**Source:** [`examples/plugin-git-ui`](https://github.com/vitejs/devtools/tree/main/examples/plugin-git-ui)
-## Real-world examples
-
-Existing DevTools integrations worth reading:
-
-- **[UnoCSS Inspector](https://github.com/unocss/unocss/blob/25c0dd737132dc20b257c276ee2bc3ccc05e2974/packages-integrations/inspector/src/index.ts#L140-L150)** — a small iframe dock entry.
-- **[vite-plugin-vue-tracer](https://github.com/antfu/vite-plugin-vue-tracer)** — an action button that triggers a DOM inspector. See the [plugin hook](https://github.com/antfu/vite-plugin-vue-tracer/blob/9f86fe723543405eea5d30588fe783796193bfd8/src/plugin.ts#L139-L157) and the [client script](https://github.com/antfu/vite-plugin-vue-tracer/blob/main/src/client/vite-devtools.ts).
-- **[`@vitejs/devtools-oxc`](https://github.com/vitejs/devtools/blob/main/packages/oxc/src/vite.ts)** — the first-party Oxc inspector: an iframe dock entry with custom RPC functions.
+## Real-world integrations
-PRs to improve coverage are welcome.
+- **[UnoCSS Inspector](https://github.com/unocss/unocss)** — a small iframe dock entry.
+- **[vite-plugin-vue-tracer](https://github.com/antfu/vite-plugin-vue-tracer)** — an action button that triggers a DOM inspector.
+- **[`@vitejs/devtools-oxc`](https://github.com/vitejs/devtools/blob/main/packages/oxc/src/vite.ts)** — the first-party Oxc inspector: an iframe dock with custom RPC functions.
diff --git a/docs/kit/index.md b/docs/kit/index.md
index c9b308adc..380da6111 100644
--- a/docs/kit/index.md
+++ b/docs/kit/index.md
@@ -4,62 +4,17 @@ outline: deep
# DevTools Kit
-DevTools Kit is the integration hub for Vite DevTools. It owns the dock, the command palette, terminal aggregation, cross-tool toasts, and the `Plugin.devtools.setup` hook that any Vite plugin can implement to surface a UI inside DevTools.
+`@vitejs/devtools-kit` is the Vite-flavored layer for building integrations that appear inside Vite DevTools. It gives a Vite plugin one extra hook — `devtools.setup(ctx)` — and a context wired with everything an integration needs: a dock to surface UI, RPC, shared state, terminals, commands, and messages.
-For a fresh Vite-specific integration, reach for `Plugin.devtools.setup` directly — that's where docks, terminals, the palette, and custom renderers live. Kit is built on [Devframe](https://devfra.me/guide/), the framework-neutral foundation; tools that already have a portable Devframe definition drop into the hub via `createPluginFromDevframe`, and standalone single-tool deployments can build on Devframe directly.
+Kit is a thin skin over [Devframe](https://devfra.me/guide/) and its hub layer. Devframe is the framework-neutral foundation that owns the mechanics — [RPC](https://devfra.me/guide/rpc), [shared state](https://devfra.me/guide/shared-state), [json-render](https://devfra.me/guide/json-render), [streaming](https://devfra.me/guide/streaming), [when clauses](https://devfra.me/references/when-clauses), and the hub's [docks, commands, messages, and terminals](https://devfra.me/guide/hub). Kit re-exports those under Vite-friendly names and adds the Vite-specific pieces: the `devtools.setup` hook, the `viteConfig` / `viteServer` context slots, the `/__devtools/` mount path, and the bridge that drops a portable devframe into Vite DevTools.

For background, see [Anthony Fu's ViteConf 2025 talk](https://www.youtube.com/watch?v=tVd0JeSr8kg).
-## What DevTools Kit provides
-
-Kit owns the hub-level surface — the things that only matter once multiple integrations share a UI:
-
-| Feature | Description |
-|---------|-------------|
-| **[DevTools Plugin](./devtools-plugin)** | The `Plugin.devtools.setup` hook, plus `createPluginFromDevframe` for porting Devframe apps into the hub. |
-| **[Dock System](./dock-system)** | The unified dock — iframe / action / custom / launcher / json-render entries — with categories, when-clauses, and remote dock support. |
-| **[Commands](./commands)** | The shared command palette: keybindings, children, when-gating across every integration. |
-| **[Messages](./messages)** | Cross-tool toast notifications and the unified messages dock. |
-| **[Terminals](./terminals)** | Aggregate terminal output from any integration into one Terminals panel. |
-| **[RPC](./rpc)** | Type-safe bidirectional RPC backed by Devframe's birpc + valibot. |
-| **[Shared State](./shared-state)** | Patch-synced state that bridges server ↔ client across every integration. |
-| **Isomorphic Views** | Deploy your UI as embedded panels, browser extensions, or standalone webpages. |
-
-## Architecture Overview
-
-```mermaid
-flowchart TB
- subgraph Browser["Browser (Client)"]
- direction TB
- subgraph DockEntries["Dock Entries"]
- Iframe["Iframe Panel"]
- Action["Action Button"]
- Custom["Custom Renderer"]
- end
- RpcClient["RPC Client"]
- DockEntries --> RpcClient
- end
-
- RpcClient <-->|WebSocket| RpcServer
-
- subgraph Server["Node.js (Server)"]
- direction TB
- RpcServer["RPC Server"]
- subgraph Context["DevTools Node Context"]
- Docks["Docks Host"]
- Views["Views Host"]
- Rpc["RPC Host"]
- State["State Host"]
- end
- RpcServer --> Context
- end
-```
-
-## Quick example
+## Two ways to build
-Authoring a Vite plugin? Add a `devtools.setup` hook and register a dock entry:
+**Author a Vite plugin.** Add a `devtools.setup` hook to any Vite plugin and register a dock entry. This is the shortest path for a Vite-only integration.
```ts
///
@@ -70,13 +25,12 @@ export default function myPlugin(): Plugin {
name: 'my-plugin',
devtools: {
setup(ctx) {
- // ctx is the kit-augmented context: rpc + docks + terminals + messages + commands
ctx.docks.register({
id: 'my-plugin',
title: 'My Plugin',
- icon: 'https://example.com/icon.svg',
+ icon: 'ph:puzzle-piece-duotone',
type: 'iframe',
- url: 'https://example.com/devtools',
+ url: '/__my-plugin/',
})
},
},
@@ -84,7 +38,7 @@ export default function myPlugin(): Plugin {
}
```
-Already have a portable Devframe app? Wrap it once and Kit synthesises the iframe dock entry from the definition's `id` / `name` / `icon` / `basePath`:
+**Bring a portable devframe.** If your tool is a [Devframe definition](https://devfra.me/guide/devframe-definition) — so it also runs standalone, as a CLI, or through a coding agent — wrap it once and Kit synthesizes the iframe dock entry for you.
```ts
// vite.config.ts
@@ -92,26 +46,32 @@ import { createPluginFromDevframe } from '@vitejs/devtools-kit/node'
import devtool from './my-devtool'
export default {
- plugins: [
- createPluginFromDevframe(devtool, {
- // Optional kit-only setup for hub features:
- setup(ctx) {
- ctx.commands.register({
- id: 'my-devtool:clear-cache',
- title: 'Clear Cache',
- handler: () => { /* ... */ },
- })
- },
- }),
- ],
+ plugins: [createPluginFromDevframe(devtool)],
}
```
-## Getting started
+## Pages
+
+Getting an integration on screen:
+
+- **[DevTools Plugin](./devtools-plugin)** — the `devtools.setup` hook, the Vite-augmented context, and hosting a static UI.
+- **[Dock System](./dock-system)** — registering dock entries and adding on-demand install launchers.
+- **[Create Plugin from Devframe](./create-plugin-from-devframe)** — mount a portable devframe as a Vite plugin.
+
+Talking between server and client:
+
+- **[Client Script & Context](./client-context)** — the injected client script and the client-side context object.
+- **[RPC](./rpc)** — type-safe, bidirectional calls between Node and the browser.
+- **[Shared State](./shared-state)** — reactive state synced across every connected client.
+
+Composing with the hub:
+
+- **[Commands](./commands)** — the shared command palette.
+- **[Messages & Notifications](./messages)** — structured entries and toasts in the Messages panel.
+- **[Structured Diagnostics](./diagnostics)** — coded errors and warnings.
+- **[Terminals & Processes](./terminals)** — spawn and stream child processes.
+- **[Examples](./examples)** — reference plugins and real-world integrations.
-1. **[DevTools Plugin](./devtools-plugin)** — register a hub plugin and walk the kit-augmented context.
-2. **[Dock System](./dock-system)** — iframe panels, action buttons, custom renderers, launchers, json-render specs.
-3. **[RPC](./rpc)** — bidirectional, type-safe communication between server and client.
-4. **[Shared State](./shared-state)** — patch-synced state that bridges every integration.
+These pages cover the Vite-plugin usage. For the deeper mechanics shared with every host framework — including [streaming](https://devfra.me/guide/streaming), [json-render](https://devfra.me/guide/json-render), and [when clauses](https://devfra.me/references/when-clauses) — see the [Devframe docs](https://devfra.me/guide/).
If you're shipping something on Kit, tag the repo with `vite-devtools` on GitHub so we can see what folks are building.
diff --git a/docs/kit/json-render.md b/docs/kit/json-render.md
deleted file mode 100644
index 7ed863db5..000000000
--- a/docs/kit/json-render.md
+++ /dev/null
@@ -1,720 +0,0 @@
----
-outline: deep
----
-
-# JSON Render
-
-JSON render panels build DevTools UIs from server-side TypeScript alone. You describe the UI as a JSON spec; the DevTools client renders it with the built-in component library.
-
-## Getting started
-
-Create a renderer handle with `ctx.createJsonRenderer()` and pass it as `ui` when registering a `json-render` dock entry:
-
-```ts
-import type { PluginWithDevTools } from '@vitejs/devtools-kit'
-
-export function MyPlugin(): PluginWithDevTools {
- return {
- name: 'my-plugin',
- devtools: {
- setup(ctx) {
- const ui = ctx.createJsonRenderer({
- root: 'root',
- elements: {
- root: {
- type: 'Stack',
- props: { direction: 'column', gap: 12 },
- children: ['heading', 'info'],
- },
- heading: {
- type: 'Text',
- props: { text: 'Hello from JSON!', variant: 'heading' },
- },
- info: {
- type: 'KeyValueTable',
- props: {
- data: {
- Version: '1.0.0',
- Status: 'Running',
- },
- },
- },
- },
- })
-
- ctx.docks.register({
- id: 'my-panel',
- title: 'My Panel',
- icon: 'ph:chart-bar-duotone',
- type: 'json-render',
- ui,
- })
- },
- },
- }
-}
-```
-
-## Spec structure
-
-A JSON render spec has three parts: a `root` element ID, an `elements` map, and an optional `state` object for two-way bindings.
-
-```ts
-ctx.createJsonRenderer({
- root: 'root',
- state: {
- searchQuery: '',
- },
- elements: {
- root: {
- type: 'Stack',
- props: { direction: 'column', gap: 12 },
- children: ['title', 'content'],
- },
- title: {
- type: 'Text',
- props: { text: 'My Panel', variant: 'heading' },
- },
- content: {
- type: 'Text',
- props: { text: 'Hello world' },
- },
- },
-})
-```
-
-Every element has a `type` (component name), `props`, and optionally `children` (array of element IDs) or `on` (event handlers).
-
-## Dynamic updates
-
-The `JsonRenderer` handle returned by `ctx.createJsonRenderer()` exposes two methods for updating the UI reactively:
-
-```ts
-const ui = ctx.createJsonRenderer(buildSpec(initialData))
-
-// Replace the entire spec (e.g. after fetching new data)
-await ui.updateSpec(buildSpec(newData))
-
-// Shallow-merge into spec.state (updates client-side state values)
-await ui.updateState({ searchQuery: 'vue' })
-```
-
-You can also update the dock entry badge when data changes:
-
-```ts
-ctx.docks.update({
- id: 'my-panel',
- type: 'json-render',
- title: 'My Panel',
- icon: 'ph:chart-bar-duotone',
- ui,
- badge: hasWarnings ? '!' : undefined,
-})
-```
-
-## Handling actions via RPC
-
-Buttons in the spec can trigger RPC functions on the server. The `on` property carries an `action` key that matches a registered RPC function name:
-
-```ts
-// In the spec — Button with an action
-const ui = ctx.createJsonRenderer({
- root: 'refresh-btn',
- elements: {
- 'refresh-btn': {
- type: 'Button',
- props: { label: 'Refresh', icon: 'ph:arrows-clockwise' },
- on: { press: { action: 'my-plugin:refresh' } },
- },
- },
-})
-```
-
-```ts
-// On the server — register the matching RPC function:
-ctx.rpc.register(defineRpcFunction({
- name: 'my-plugin:refresh',
- type: 'action',
- setup: ctx => ({
- handler: async () => {
- const data = await fetchData()
- await ui.updateSpec(buildSpec(data))
- },
- }),
-}))
-```
-
-You can pass parameters from the spec to the action handler:
-
-```ts
-const ui = ctx.createJsonRenderer({
- root: 'delete-btn',
- elements: {
- 'delete-btn': {
- type: 'Button',
- props: { label: 'Delete', variant: 'danger' },
- on: {
- press: {
- action: 'my-plugin:delete',
- params: { id: 'some-id' },
- },
- },
- },
- },
-})
-```
-
-## State and two-way binding
-
-`$bindState` on a TextInput `value` creates a two-way binding with a state key; `$state` reads the bound value in action params:
-
-```ts
-const ui = ctx.createJsonRenderer({
- root: 'root',
- state: { message: '' },
- elements: {
- root: {
- type: 'Stack',
- props: { direction: 'row', gap: 8 },
- children: ['input', 'submit'],
- },
- input: {
- type: 'TextInput',
- props: {
- placeholder: 'Type here...',
- value: { $bindState: '/message' },
- },
- },
- submit: {
- type: 'Button',
- props: { label: 'Submit', variant: 'primary' },
- on: {
- press: {
- action: 'my-plugin:submit',
- params: { text: { $state: '/message' } },
- },
- },
- },
- },
-})
-```
-
-The server-side handler receives the resolved state values:
-
-```ts
-ctx.rpc.register(defineRpcFunction({
- name: 'my-plugin:submit',
- type: 'action',
- setup: ctx => ({
- handler: async (params: { text?: string }) => {
- console.log('User submitted:', params.text)
- },
- }),
-}))
-```
-
-## Built-in components
-
-An element whose `type` doesn't match any of the components below — e.g. a spec authored against a newer base-catalog version than the connected client implements, or a plain typo — renders as a visible "Unsupported component" placeholder instead of disappearing silently, so a mismatch is easy to spot during development.
-
-### Layout
-
-#### Stack
-
-Flex layout container. Arranges children vertically or horizontally.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `direction` | `'row' \| 'column'` | `'column'` | Layout direction |
-| `gap` | `number` | `8` | Gap between children in pixels |
-| `align` | `'start' \| 'center' \| 'end' \| 'stretch'` | — | Cross-axis alignment |
-| `justify` | `'start' \| 'center' \| 'end' \| 'between' \| 'around'` | — | Main-axis alignment |
-| `wrap` | `boolean` | `false` | Allow children to wrap onto multiple lines |
-| `flex` | `number \| string` | — | `flex` shorthand for the container |
-| `padding` | `number` | — | Padding in pixels |
-| `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger' \| 'info' \| 'success' \| 'warning'` | `'primary'` | Background tint (`danger`/`info`/`success`/`warning` also draw a matching border) |
-| `interactive` | `boolean` | `false` | Tint the background on hover — useful for clickable-looking rows |
-
-
-```ts
-// Horizontal toolbar with items spaced apart
-{
- type: 'Stack',
- props: { direction: 'row', gap: 8, justify: 'between', align: 'center' },
- children: ['title', 'actions'],
-}
-```
-
-
-```ts
-// Vertical form layout
-{
- type: 'Stack',
- props: { direction: 'column', gap: 12, padding: 16 },
- children: ['name-input', 'email-input', 'submit-btn'],
-}
-```
-
-#### Card
-
-Container with an optional title and collapsible behavior.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `title` | `string` | — | Header title. A collapsible card still shows its (empty) header without one — otherwise there'd be nothing to click |
-| `collapsible` | `boolean` | `false` | Whether the card can be collapsed |
-| `defaultCollapsed` | `boolean` | `false` | Start collapsed (when `collapsible`) |
-| `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger' \| 'info' \| 'success' \| 'warning'` | `'primary'` | A light background tint on the body, a stronger same-hue tint on the header bar, and a matching border (`danger`/`info`/`success`/`warning`) |
-| `interactive` | `boolean` | `false` | Brighten the border on hover |
-| `loading` | `boolean` | `false` | Show a loading state |
-
-
-```ts
-{
- type: 'Card',
- props: { title: 'Build Info', collapsible: true },
- children: ['info-table'],
-}
-```
-
-#### Tabs
-
-Switches which of its `children` is shown, one tab per entry in `tabs` (positionally matched — `children[i]` renders when `tabs[i]` is active).
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `tabs` | `Array<{ value: string, label: string, icon?: string, badge?: string, badgeVariant?: 'default' \| 'info' \| 'success' \| 'warning' \| 'danger' }>` | — | The tab list |
-| `value` | `string` | — | Active tab's `value` (use `$bindState` for two-way binding) |
-| `defaultValue` | `string` | first tab | Initial active tab when `value` isn't bound |
-| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Underlined top bar vs. a left-hand rail |
-
-**Event**: `change` — fires when the active tab changes (including via arrow-key navigation).
-
-
-```ts
-{
- root: 'root',
- elements: {
- root: {
- type: 'Tabs',
- props: {
- tabs: [
- { value: 'mfe', label: 'Micro-Frontends', badge: '3', badgeVariant: 'success' },
- { value: 'gateway', label: 'Gateway' },
- ],
- },
- children: ['mfe-panel', 'gateway-panel'],
- },
- 'mfe-panel': { type: 'Text', props: { text: '3 active overrides' } },
- 'gateway-panel': { type: 'Text', props: { text: 'No overrides' } },
- },
-}
-```
-
-#### Divider
-
-Visual separator line with an optional label.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `label` | `string` | — | Text shown in the middle of the divider |
-
-
-```ts
-{
- type: 'Divider',
- props: { label: 'Advanced' },
-}
-```
-
-### Typography
-
-#### Text
-
-Display text with different visual styles.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `text` | `string` | — | Text content |
-| `variant` | `'heading' \| 'subheading' \| 'body' \| 'caption' \| 'code'` | `'body'` | Visual style |
-| `weight` | `'normal' \| 'medium' \| 'bold'` | — | Font weight |
-| `color` | `'base' \| 'muted' \| 'faint' \| 'primary' \| 'success' \| 'warning' \| 'danger'` | — | Text color |
-
-
-```ts
-// heading — 16px bold
-{ type: 'Text', props: { text: 'Module Graph', variant: 'heading' } }
-
-// body (default) — 13px
-{ type: 'Text', props: { text: 'Visualize module dependencies' } }
-
-// caption — 12px, muted
-{ type: 'Text', props: { text: 'Click a node to inspect', variant: 'caption' } }
-
-// code — monospace with background
-{ type: 'Text', props: { text: 'src/index.ts', variant: 'code' } }
-```
-
-#### Icon
-
-Renders an [Iconify](https://icon-sets.iconify.design/) icon by name.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `name` | `string` | — | Iconify icon name (e.g. `'ph:gear'`) |
-| `size` | `number` | `20` | Icon size in pixels |
-
-
-```ts
-{ type: 'Icon', props: { name: 'ph:check-circle', size: 16 } }
-```
-
-#### Badge
-
-Status label with semantic color variants.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `text` | `string` | — | Badge text |
-| `variant` | `'default' \| 'info' \| 'success' \| 'warning' \| 'danger'` | `'default'` | Color variant |
-| `minWidth` | `number` | — | Minimum width in pixels |
-
-
-```ts
-{ type: 'Badge', props: { text: 'Ready', variant: 'success' } }
-{ type: 'Badge', props: { text: '3 warnings', variant: 'warning' } }
-{ type: 'Badge', props: { text: 'Failed', variant: 'danger' } }
-```
-
-### Inputs
-
-#### Button
-
-Clickable button that triggers an action via the `press` event.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `label` | `string` | — | Button text |
-| `icon` | `string` | — | Iconify icon name |
-| `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger'` | `'secondary'` | Visual style |
-| `disabled` | `boolean` | `false` | Disable interaction |
-| `loading` | `boolean` | `false` | Show a loading state |
-
-**Event**: `press` — fires when the button is clicked.
-
-
-```ts
-// Label + icon
-{ type: 'Button', props: { label: 'Refresh', icon: 'ph:arrows-clockwise' }, on: { press: { action: 'my-plugin:refresh' } } }
-
-// Danger variant
-{ type: 'Button', props: { label: 'Clear Cache', variant: 'danger', icon: 'ph:trash' }, on: { press: { action: 'my-plugin:clear-cache' } } }
-
-// Icon-only ghost button
-{ type: 'Button', props: { icon: 'ph:plus', variant: 'ghost' }, on: { press: { action: 'my-plugin:add' } } }
-```
-
-#### Link
-
-Links to `http`, `https` and `mailto` targets — anything else falls back to rendering `label` as plain text.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `href` | `string` | — | Link target |
-| `label` | `string` | — | Link text (defaults to `href`) |
-| `icon` | `string` | — | Iconify icon name |
-| `external` | `boolean` | `true` for `http(s)` | Open in a new tab |
-
-
-```ts
-{ type: 'Link', props: { href: 'https://vite.dev', label: 'Vite docs', icon: 'ph:arrow-square-out' } }
-```
-
-#### TextInput
-
-Text input field with optional two-way state binding.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `placeholder` | `string` | — | Placeholder text |
-| `value` | `string` | — | Current value (use `$bindState` for two-way binding) |
-| `label` | `string` | — | Label shown above the input |
-| `type` | `'text' \| 'search' \| 'number' \| 'password' \| 'email'` | `'text'` | Input type |
-| `disabled` | `boolean` | `false` | Disable interaction |
-| `loading` | `boolean` | `false` | Show a loading state |
-
-
-```ts
-{
- type: 'TextInput',
- props: {
- placeholder: 'Search modules...',
- value: { $bindState: '/query' },
- },
-}
-```
-
-#### Select
-
-Dropdown choosing one value from a fixed set of options, with optional two-way state binding.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `value` | `string` | — | Current value (use `$bindState` for two-way binding) |
-| `options` | `(string \| { value, label?, icon?, description? })[]` | — | Available choices |
-| `placeholder` | `string` | — | Shown while `value` is unset |
-| `label` | `string` | — | Label shown above the select |
-| `disabled` | `boolean` | `false` | Disable interaction |
-| `searchable` | `boolean` | `false` | Add a substring filter box to the panel |
-
-**Event**: `change` — fires when the selected value changes.
-
-
-```ts
-{
- type: 'Select',
- props: {
- label: 'Environment',
- value: { $bindState: '/env' },
- options: [
- { value: 'dev', label: 'Development' },
- { value: 'staging', label: 'Staging' },
- { value: 'prod', label: 'Production', description: 'Live traffic' },
- ],
- },
- on: { change: { action: 'my-plugin:switch-env' } },
-}
-```
-
-See [State and Two-Way Binding](#state-and-two-way-binding) for a full example.
-
-### Data display
-
-#### KeyValueTable
-
-Display key-value pairs in a two-column table.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `data` | `Record` | — | Key-value pairs to display |
-| `loading` | `boolean` | `false` | Show a loading state |
-
-
-```ts
-{
- type: 'KeyValueTable',
- props: {
- data: {
- Mode: 'production',
- Duration: '1.2s',
- Modules: '142',
- Output: 'dist/',
- },
- },
-}
-```
-
-#### DataTable
-
-Tabular data with configurable columns and scroll support.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `columns` | `Array` | — | Column definitions (a bare string uses the key as its label) |
-| `rows` | `Array>` | — | Row data |
-| `height` | `number` | — | Scrollable max height in pixels |
-| `loading` | `boolean` | `false` | Show a loading state |
-
-
-```ts
-{
- type: 'DataTable',
- props: {
- columns: [
- { key: 'id', label: 'Module' },
- { key: 'size', label: 'Size' },
- { key: 'time', label: 'Transform' },
- ],
- rows: [
- { id: 'src/index.ts', size: '2.1 KB', time: '12ms' },
- { id: 'src/utils.ts', size: '0.8 KB', time: '3ms' },
- { id: 'src/app.vue', size: '4.5 KB', time: '45ms' },
- ],
- height: 400,
- },
-}
-```
-
-#### CodeBlock
-
-Display a code snippet with an optional filename header.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `code` | `string` | — | Code content |
-| `language` | `string` | — | Language identifier |
-| `filename` | `string` | — | Filename shown as header |
-| `height` | `number` | — | Scrollable max height in pixels |
-
-
-```ts
-{
- type: 'CodeBlock',
- props: {
- code: 'export default defineConfig({\n plugins: [vue()],\n})',
- language: 'ts',
- filename: 'vite.config.ts',
- height: 200,
- },
-}
-```
-
-#### Progress
-
-Progress bar with a percentage label.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `value` | `number` | — | Current value |
-| `max` | `number` | `100` | Maximum value |
-| `label` | `string` | — | Label text |
-
-
-```ts
-{ type: 'Progress', props: { value: 73, max: 100, label: 'Build progress' } }
-```
-
-#### Tree
-
-Expandable tree view for inspecting nested objects.
-
-| Prop | Type | Default | Description |
-|------|------|---------|-------------|
-| `data` | `unknown` | — | Any JSON-serializable value |
-| `defaultExpanded` | `boolean` | `true` | Whether nodes start expanded |
-
-
-```ts
-{
- type: 'Tree',
- props: {
- data: {
- build: {
- outDir: 'dist',
- minify: true,
- rollupOptions: { external: ['vue'] },
- },
- server: { port: 3000, hmr: true },
- },
- defaultExpanded: true,
- },
-}
-```
-
-## Full example
-
-A complete panel combining layout, data display, inputs, and actions:
-
-```ts
-import type { JsonRenderSpec, PluginWithDevTools } from '@vitejs/devtools-kit'
-import { defineRpcFunction } from '@vitejs/devtools-kit'
-
-function buildSpec(data: { modules: number, time: string, size: string }): JsonRenderSpec {
- return {
- root: 'root',
- state: { filter: '' },
- elements: {
- 'root': {
- type: 'Stack',
- props: { direction: 'column', gap: 12, padding: 8 },
- children: ['header', 'divider', 'stats', 'modules'],
- },
- 'header': {
- type: 'Stack',
- props: { direction: 'row', gap: 8, align: 'center', justify: 'between' },
- children: ['title', 'refresh-btn'],
- },
- 'title': {
- type: 'Text',
- props: { text: 'Build Report', variant: 'heading' },
- },
- 'refresh-btn': {
- type: 'Button',
- props: { label: 'Refresh', icon: 'ph:arrows-clockwise' },
- on: { press: { action: 'build-report:refresh' } },
- },
- 'divider': {
- type: 'Divider',
- props: {},
- },
- 'stats': {
- type: 'Card',
- props: { title: 'Summary' },
- children: ['stats-table'],
- },
- 'stats-table': {
- type: 'KeyValueTable',
- props: {
- data: {
- 'Total Modules': String(data.modules),
- 'Build Time': data.time,
- 'Output Size': data.size,
- },
- },
- },
- 'modules': {
- type: 'Card',
- props: { title: 'Modules', collapsible: true },
- children: ['module-table'],
- },
- 'module-table': {
- type: 'DataTable',
- props: {
- columns: [
- { key: 'name', label: 'Module' },
- { key: 'size', label: 'Size' },
- ],
- rows: [
- { name: 'src/index.ts', size: '2.1 KB' },
- { name: 'src/app.vue', size: '4.5 KB' },
- ],
- height: 300,
- },
- },
- },
- }
-}
-
-export function BuildReportPlugin(): PluginWithDevTools {
- return {
- name: 'build-report',
- devtools: {
- setup(ctx) {
- const data = { modules: 142, time: '1.2s', size: '48 KB' }
- const ui = ctx.createJsonRenderer(buildSpec(data))
-
- ctx.docks.register({
- id: 'build-report',
- title: 'Build Report',
- icon: 'ph:chart-bar-duotone',
- type: 'json-render',
- ui,
- })
-
- ctx.rpc.register(defineRpcFunction({
- name: 'build-report:refresh',
- type: 'action',
- setup: ctx => ({
- handler: async () => {
- const newData = { modules: 145, time: '1.1s', size: '47 KB' }
- await ui.updateSpec(buildSpec(newData))
- },
- }),
- }))
- },
- },
- }
-}
-```
-
-For a more advanced plugin using json-render with per-file actions, text input with state binding, and dynamic badge updates, see the [Git UI example](/kit/examples#git-ui).
diff --git a/docs/kit/messages.md b/docs/kit/messages.md
index d982af41b..96fd44c02 100644
--- a/docs/kit/messages.md
+++ b/docs/kit/messages.md
@@ -1,215 +1,49 @@
-# Messages & Notifications
-
-The Messages system lets plugins emit structured message entries from both the server (Node.js) and client (browser) contexts. Entries appear in the **Messages** panel (the official [`@devframes/plugin-messages`](https://devfra.me), mounted as a built-in) and can optionally surface as toast notifications. For *coded* errors and warnings with stable codes and docs URLs, use [Structured Diagnostics](./diagnostics) (`ctx.diagnostics`) instead.
-
-## Use cases
-
-- **Accessibility audits** — run a11y checks on the client and report warnings with element positions.
-- **Runtime errors** — capture and display errors with stack traces.
-- **Linting & testing** — run ESLint or test runners alongside the dev server and surface results with file positions.
-- **Notifications** — short-lived messages like "URL copied" that auto-dismiss.
+---
+outline: deep
+---
-## Message entry fields
-
-| Field | Type | Required | Description |
-|-------|------|----------|-------------|
-| `message` | `string` | Yes | Short title or summary |
-| `level` | `'info' \| 'warn' \| 'error' \| 'success' \| 'debug'` | Yes | Severity level, determines color and icon |
-| `description` | `string` | No | Detailed description or explanation |
-| `stacktrace` | `string` | No | Stack trace string |
-| `filePosition` | `{ file, line?, column? }` | No | Source file location (clickable in the panel) |
-| `elementPosition` | `{ selector?, boundingBox?, description? }` | No | DOM element position info |
-| `notify` | `boolean` | No | Show as a toast notification |
-| `category` | `string` | No | Grouping category (e.g., `'a11y'`, `'lint'`) |
-| `labels` | `string[]` | No | Tags for filtering |
-| `autoDismiss` | `number` | No | Time in ms to auto-dismiss the toast (default: 5000) |
-| `autoDelete` | `number` | No | Time in ms to auto-delete the message entry |
-| `status` | `'loading' \| 'idle'` | No | Status indicator (shows spinner when `'loading'`) |
-| `id` | `string` | No | Explicit id for deduplication — re-adding with the same id updates the existing entry |
+# Messages & Notifications
-The `from` field is set to `'server'` or `'browser'` automatically, based on where the message was emitted.
+Messages are structured entries a plugin emits from either the server or the client. They appear in the **Messages** panel (the official [`@devframes/plugin-messages`](https://devfra.me/guide/hub), mounted as a built-in) and can optionally surface as toast notifications — a11y warnings with element positions, runtime errors with stack traces, lint/test results, or short "URL copied" toasts.
-## Usage
+For coded errors and warnings with stable codes and docs URLs, use [Structured Diagnostics](./diagnostics) instead.
-Server and client share the same `context.messages` API. Methods return Promises; for fire-and-forget usage, skip the `await`.
+## Emit a message
-### Fire-and-forget
+Server and client share the same `ctx.messages` API. Fire-and-forget by skipping the `await`:
```ts
-// No await needed — just emit the message
-context.messages.add({
+ctx.messages.add({
message: 'Plugin initialized',
level: 'info',
})
```
-### With handle
-
-`await` the `add()` call for a `DevToolsMessageHandle` you can update later:
+`await` the call for a handle you can update later — handy for progress:
```ts
-// Await to get a handle for later updates
-const handle = await context.messages.add({
+const handle = await ctx.messages.add({
id: 'my-build',
- message: 'Building...',
+ message: 'Building…',
level: 'info',
status: 'loading',
})
-// Later, update via the handle
-await handle.update({
- message: 'Build complete',
- level: 'success',
- status: 'idle',
-})
-
-// Or dismiss it
-await handle.dismiss()
+await handle.update({ message: 'Build complete', level: 'success', status: 'idle' })
```
-### Server-side example
-
-```ts
-export function myPlugin() {
- return {
- name: 'my-plugin',
- devtools: {
- setup(context) {
- // Fire-and-forget
- context.messages.add({
- message: 'Plugin initialized',
- level: 'info',
- })
- },
- },
- }
-}
-```
-
-### Client-side example
-
-```ts
-import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'
-
-export default async function (context: DockClientScriptContext) {
- // Await to get the handle
- const message = await context.messages.add({
- message: 'Running audit...',
- level: 'info',
- status: 'loading',
- notify: true,
- })
-
- // ... do work ...
-
- // Update via handle — can also be fire-and-forget
- message.update({
- message: 'Audit complete — 3 issues found',
- level: 'warn',
- status: 'idle',
- })
-}
-```
-
-## Message handle
-
-`context.messages.add()` returns a `Promise` with:
-
-| Property/Method | Description |
-|-----------------|-------------|
-| `handle.id` | The message entry id |
-| `handle.entry` | The current `DevToolsMessageEntry` data |
-| `handle.update(patch)` | Partially update the message entry (returns `Promise`) |
-| `handle.dismiss()` | Remove the message entry (returns `Promise`) |
-
-Both `handle.update()` and `handle.dismiss()` return Promises; the `await` is optional.
-
-## Deduplication
-
-Calling `add()` with an explicit `id` that already exists updates the existing entry rather than creating a duplicate — useful for ongoing operations:
-
-```ts
-// First call creates the entry
-context.messages.add({ id: 'my-scan', message: 'Scanning...', level: 'info', status: 'loading' })
-
-// Second call with same id updates it
-context.messages.add({ id: 'my-scan', message: 'Scan complete', level: 'success', status: 'idle' })
-```
+## Common fields
-## Toast notifications
+| Field | Description |
+|-------|-------------|
+| `message` | Short title (required) |
+| `level` | `'info' \| 'warn' \| 'error' \| 'success' \| 'debug'` (required) |
+| `description` | Longer explanation |
+| `filePosition` | `{ file, line?, column? }` — clickable source location |
+| `elementPosition` | `{ selector?, boundingBox?, description? }` — DOM target |
+| `notify` | Show as a toast |
+| `category` / `labels` | Grouping and filter tags |
+| `autoDismiss` | Toast lifetime in ms (default 5000) |
+| `id` | Explicit id for dedup — re-adding updates the existing entry |
-Set `notify: true` to surface the message entry as a toast overlay. Toasts appear whether or not the Messages panel is open.
-
-```ts
-context.messages.add({
- message: 'URL copied to clipboard',
- level: 'success',
- notify: true,
- autoDismiss: 2000, // disappear after 2 seconds
-})
-```
-
-Toast auto-dismiss defaults to 5 seconds.
-
-## Managing messages
-
-```ts
-// Remove a specific message by id
-context.messages.remove(entryId)
-
-// Clear all messages
-context.messages.clear()
-```
-
-Capacity tops out at 1000 entries; the oldest are dropped automatically when the limit is hit.
-
-The [A11y Checker example](/kit/examples#a11y-checker) is a plugin that uses messages to report accessibility violations with severity levels, element positions, and WCAG labels.
-
-## Events
-
-The host emits events for observers of the message stream:
-
-```ts
-ctx.messages.events.on('message:added', (entry) => { /* ... */ })
-ctx.messages.events.on('message:updated', (entry) => { /* ... */ })
-ctx.messages.events.on('message:removed', (id) => { /* ... */ })
-ctx.messages.events.on('message:cleared', () => { /* ... */ })
-```
-
-Use the events to bridge messages into external tools — mirror them into a structured log, forward certain categories to your own reporter, etc.:
-
-```ts
-ctx.messages.events.on('message:added', (entry) => {
- if (entry.category === 'a11y')
- console.log('a11y finding:', entry.message)
-})
-```
-
-## Long-running operation pattern
-
-Combine `id`-based deduplication with `status: 'loading'` to drive a single message through a multi-step lifecycle:
-
-```ts
-async function rebuild(ctx) {
- const handle = await ctx.messages.add({
- id: 'my-plugin:rebuild',
- message: 'Rebuilding...',
- level: 'info',
- status: 'loading',
- })
-
- try {
- await doRebuild()
- await handle.update({ message: 'Rebuild complete', level: 'success', status: 'idle' })
- }
- catch (error) {
- await handle.update({
- message: 'Rebuild failed',
- level: 'error',
- description: (error as Error).message,
- stacktrace: (error as Error).stack,
- status: 'idle',
- })
- }
-}
-```
+The `from` field (`'server'` or `'browser'`) is set automatically based on where you emitted the message.
diff --git a/docs/kit/remote-client.md b/docs/kit/remote-client.md
deleted file mode 100644
index 5f95feb81..000000000
--- a/docs/kit/remote-client.md
+++ /dev/null
@@ -1,215 +0,0 @@
----
-outline: deep
----
-
-# Remote Client
-
-Remote client mode points a dock at a hosted website (e.g. `https://example.com/devtools`) instead of bundling a SPA dist with your plugin. The hosted page opens a WebSocket back to the local Vite dev server and uses the same RPC and shared-state APIs as an embedded client. A live demo lives at [Remote Connection Demo](./remote-demo) — register a dock pointing at that URL to see the flow end-to-end.
-
-Compared to the bundled approach in [Dock System → Iframe Panels](./dock-system#iframe-panels), remote mode:
-
-- **Keeps your npm package small.** Ship node-side code only.
-- **Decouples release cadences.** Update the hosted UI without republishing the plugin.
-- **Surfaces existing dashboards.** Drop in a production URL your team already runs.
-
-The tradeoff: users need to be online to load the hosted page, and the hosted origin gets trusted access to render local data.
-
-## How it works
-
-When you register an iframe dock with `remote: true`, DevTools:
-
-1. Allocates a session-only, pre-approved auth token for that dock.
-2. Injects a connection descriptor — the WS URL, the token, and the user's dev-server origin — into the iframe's `src` attribute.
-3. Accepts the token on WebSocket handshake (after verifying the `Origin` header, if origin-lock is on).
-
-On the hosted page, `connectRemoteDevTools()` parses the descriptor out of the URL and returns a fully connected [`DevToolsRpcClient`](./rpc) — the same client you'd get from `getDevToolsRpcClient()` in an embedded page.
-
-```mermaid
-sequenceDiagram
- participant User as User app (localhost:5173)
- participant Core as DevTools core
- participant Hosted as Hosted page (example.com)
-
- User->>Core: configureServer
- Core->>Core: register dock (remote: true) → allocate token
- Core->>Core: createWsServer → wsEndpoint=ws://localhost:5173/__devtools/__ws
- User->>Core: open dock
- Core->>Hosted: iframe src + connection descriptor
- Hosted->>Core: WS connect (?devframe_auth_token=...)
- Core->>Core: verify token + Origin
- Core-->>Hosted: trusted session
- Hosted->>Core: rpc.call('my-plugin:…')
-```
-
-## Registering a remote dock
-
-```ts
-import type { Plugin } from 'vite'
-
-export function myPlugin(): Plugin {
- return {
- name: 'my-plugin',
- devtools: {
- setup(ctx) {
- ctx.docks.register({
- id: 'my-remote-tool',
- title: 'My Tool',
- icon: 'ph:cloud-duotone',
- type: 'iframe',
- url: 'https://example.com/devtools',
- remote: true,
- })
- },
- },
- }
-}
-```
-
-That's the whole node-side change. The dock renders like a regular iframe panel, with the connection descriptor appended invisibly to the URL.
-
-### Options
-
-```ts
-interface RemoteDockOptions {
- /** @default 'fragment' */
- transport?: 'fragment' | 'query'
- /** @default true */
- originLock?: boolean
-}
-
-// in ctx.docks.register({ ... }):
-// remote: true
-// or:
-// remote: { transport: 'query', originLock: false }
-```
-
-#### `transport`
-
-- **`'fragment'` (default)** — the descriptor rides as a URL fragment (`#vite-devtools-kit-connection=...`). Fragments stay client-side: they don't reach servers, don't enter access logs, and get stripped from `Referer` on sub-resource requests. The safest place to carry an auth token.
-- **`'query'`** — the descriptor rides as a query parameter (`?vite-devtools-kit-connection=...`). Pick this when your SPA router uses the fragment for navigation, or when your hosting platform / CDN rewrites URLs in a way that drops fragments.
-
-> [!WARNING]
-> With `'query'` transport, the auth token appears in server access logs and outbound `Referer` headers. Use it only when you control the analytics / log pipeline on the hosted origin.
-
-#### `originLock`
-
-When on (default), the WebSocket handshake is rejected if the browser's `Origin` header doesn't match the origin of the registered dock URL. If the token leaks (for example to an external analytics tool that ingests URLs), the wrong origin can't reuse it.
-
-Turn it off only when the same hosted app is served from multiple origins (e.g. preview deploys on `pr-123.preview.example.com`):
-
-```ts
-ctx.docks.register({
- id: 'my-remote-tool',
- title: 'My Tool',
- icon: 'ph:cloud-duotone',
- type: 'iframe',
- url: 'https://example.com/devtools',
- remote: { originLock: false },
-})
-```
-
-## Connecting from the hosted page
-
-Install `@vitejs/devtools-kit` as a dependency of your hosted page — the client entrypoint is browser-safe:
-
-```sh
-pnpm add @vitejs/devtools-kit
-```
-
-Then, on page load:
-
-```ts
-import { connectRemoteDevTools } from '@vitejs/devtools-kit/client'
-
-const rpc = await connectRemoteDevTools()
-
-// From here, use it like any other DevToolsRpcClient:
-const data = await rpc.call('my-plugin:get-data')
-```
-
-`connectRemoteDevTools()` reads the descriptor from the current URL, opens the WebSocket, and resolves to a `DevToolsRpcClient` with `.call`, `.callEvent`, `.callOptional`, `.sharedState`, and the rest of the standard API documented in [RPC](./rpc).
-
-When someone opens the page directly (no descriptor in the URL), the call throws. Use that as a cue to render a friendly "Open me through Vite DevTools" placeholder:
-
-```ts
-import { connectRemoteDevTools, parseRemoteConnection } from '@vitejs/devtools-kit/client'
-
-if (!parseRemoteConnection()) {
- renderStandaloneLandingPage()
-}
-else {
- const rpc = await connectRemoteDevTools()
- renderConnectedUi(rpc)
-}
-```
-
-### Advanced: custom URL / options
-
-`connectRemoteDevTools` forwards any [`DevToolsRpcClientOptions`](./rpc) — RPC caching, custom `rpcOptions`, and so on — while keeping `connectionMeta` and `authToken` sourced from the descriptor.
-
-```ts
-const rpc = await connectRemoteDevTools({
- cacheOptions: { maxAge: 5000 },
-})
-```
-
-For testing or non-browser environments, pass an explicit URL or raw fragment/query string to `parseRemoteConnection`:
-
-```ts
-parseRemoteConnection('https://example.com/p#vite-devtools-kit-connection=...')
-parseRemoteConnection('?vite-devtools-kit-connection=...')
-```
-
-## Descriptor shape
-
-The descriptor is a superset of [`ConnectionMeta`](./rpc), so `getDevToolsRpcClient({ connectionMeta })` accepts a parsed descriptor directly:
-
-```ts
-interface RemoteConnectionInfo {
- v: 1
- backend: 'websocket'
- /** Full ws:// or wss:// URL. */
- websocket: string
- authToken: string
- /** Dev-server origin, e.g. http://localhost:5173. */
- origin: string
-}
-```
-
-It's JSON-encoded and base64url-encoded, then appended to the iframe URL under the parameter name `vite-devtools-kit-connection`.
-
-## Trust boundary
-
-Remote mode extends the following trust chain:
-
-1. The user installs your plugin and opts into DevTools.
-2. Your plugin declares a remote URL.
-3. DevTools hands the hosted origin a session token scoped to that URL.
-
-Properties of the session token:
-
-- **Pre-approved.** No interactive "trust this browser?" prompt fires; the user agreed to the integration when they installed the plugin.
-- **Session-scoped.** Stored in memory only and regenerated on every dev-server restart.
-- **Re-register-scoped.** Calling `ctx.docks.register(...)` again for the same id revokes the previous token; live WS clients on the old token receive `devframe:auth:revoked` and become untrusted.
-- **Origin-locked by default.** Only connections whose `Origin` matches the dock URL are accepted.
-
-> [!WARNING]
-> The token rides in the URL — treat it as a session secret. Avoid logging URLs to external services on the hosted page, and prefer `transport: 'fragment'` unless you have a specific reason to use `'query'`.
-
-## Build mode
-
-The WebSocket server runs in dev mode (`vite`); remote-iframe docks skip themselves in static-dump output, so no [`when` clause](./when-clauses) is needed. Add one if you want different visibility in embedded vs. standalone clients:
-
-```ts
-ctx.docks.register({
- // ...
- remote: true,
- when: 'clientType == embedded',
-})
-```
-
-## Related
-
-- [Dock System](./dock-system) — the full list of dock types.
-- [RPC](./rpc) — the `DevToolsRpcClient` API.
-- [When Clauses](./when-clauses) — conditional dock visibility.
diff --git a/docs/kit/remote-demo.md b/docs/kit/remote-demo.md
index 38f2511f7..c40f532df 100644
--- a/docs/kit/remote-demo.md
+++ b/docs/kit/remote-demo.md
@@ -175,7 +175,7 @@ ctx.docks.register({
1. Your plugin registers an iframe dock with `remote: true` pointing at this page.
2. The DevTools core allocates a session-only auth token and appends the connection descriptor to the iframe URL: `#vite-devtools-kit-connection=…`
-3. When this page loads, [`parseRemoteConnection()`](./remote-client#connect-from-the-hosted-page) reads the descriptor, and [`connectRemoteDevTools()`](./remote-client#connect-from-the-hosted-page) opens a WebSocket back to the local dev server.
+3. When this page loads, `parseRemoteConnection()` reads the descriptor, and `connectRemoteDevTools()` opens a WebSocket back to the local dev server.
4. The live "dock registry" list above subscribes to the `devframe:docks` shared state — it re-renders on every registration change.
-See the [Remote Client guide](./remote-client) for the full API and the underlying security model.
+Both helpers are exported from `@vitejs/devtools-kit/client`. See the [Devframe client guide](https://devfra.me/guide/client) for the browser RPC client it builds on.
diff --git a/docs/kit/rpc.md b/docs/kit/rpc.md
index 3b2c6c538..b8ff66cac 100644
--- a/docs/kit/rpc.md
+++ b/docs/kit/rpc.md
@@ -2,27 +2,13 @@
outline: deep
---
-# Remote Procedure Calls (RPC)
+# RPC
-DevTools Kit's RPC layer is type-safe, bidirectional, and works between your Node.js server and any connected browser client.
+RPC is how a DevTools integration's Node side and its browser UI talk to each other — type-safe and bidirectional. Define functions on the server with `defineRpcFunction`, then `call()` them from the client.
-## Overview
+The full API (schema validation, the client, birpc internals) is documented in the [Devframe RPC guide](https://devfra.me/guide/rpc); this page shows the shapes you reach for from a Vite plugin.
-```mermaid
-sequenceDiagram
- participant Client as Browser Client
- participant Server as Node.js Server
-
- Client->>Server: rpc.call('my-plugin:get-data', id)
- Note over Server: handler: async (id) => fetchData(id)
- Server->>Client: { id, data: '...' }
-```
-
-## Server-side functions
-
-### Defining RPC functions
-
-Use `defineRpcFunction` for type-safe server functions:
+## Define a function
```ts
import { defineRpcFunction } from '@vitejs/devtools-kit'
@@ -31,602 +17,44 @@ const getModules = defineRpcFunction({
name: 'my-plugin:get-modules',
type: 'query',
setup: ctx => ({
- handler: async () => {
- // Access DevTools context
- console.log('Mode:', ctx.mode)
-
- return [
- { id: '/src/main.ts', size: 1024 },
- { id: '/src/App.vue', size: 2048 },
- ]
- },
+ handler: async () => [
+ { id: '/src/main.ts', size: 1024 },
+ { id: '/src/App.vue', size: 2048 },
+ ],
}),
})
```
-### Naming convention
-
-Scope each function with your package prefix and use kebab-case for the function part: `my-plugin:get-modules`, `my-plugin:read-file`.
-
-### Function types
-
-| Type | Use for | Cached | Dump support |
-|------|---------|--------|--------------|
-| `query` | Fetch data, read operations | Yes | Manual |
-| `static` | Constant data | Indefinitely | Automatic |
-| `action` | Side effects, mutations | No | — |
-| `event` | Notifications without a response | No | — |
-
-For chunk-style data (LLM deltas, log lines, build progress, file uploads), reach for [streaming channels](./streaming) — they handle stream IDs, cancellation, replay, and Web Streams interop for you.
-
-### Handler arguments
-
-Handlers can accept any serializable arguments:
-
-```ts
-const getModule = defineRpcFunction({
- name: 'my-plugin:get-module',
- type: 'query',
- setup: () => ({
- handler: async (id: string, options?: { includeSource: boolean }) => {
- // id and options are passed from the client
- return { id, source: options?.includeSource ? '...' : undefined }
- },
- }),
-})
-```
-
-### Context in setup
-
-The `setup` function receives the full `ViteDevToolsNodeContext`:
-
-```ts
-setup: (ctx) => {
- // Access Vite config
- const root = ctx.viteConfig.root
-
- // Access dev server (if in dev mode)
- const server = ctx.viteServer
-
- return {
- handler: async () => {
- // Use ctx here too
- return { root, mode: ctx.mode }
- },
- }
-}
-```
-
-For build-mode compatibility, compute data in `setup` using the context and let the handler use it. The dump feature then pre-computes results at build time using values that already exist in `setup`'s closure.
-
-### Registering functions
-
-Register the RPC function from `devtools.setup`:
+Register it in `devtools.setup`:
```ts
const plugin: Plugin = {
devtools: {
setup(ctx) {
ctx.rpc.register(getModules)
- }
- }
-}
-```
-
-### Dump feature for build mode
-
-A static DevTools build (via `vite devtools build` or the [`build.withApp`](/guide/#building-with-the-app) plugin option) has no live server. The dump feature pre-computes RPC results at build time and bakes them into the static output.
-
-#### How it works
-
-1. At build time, `dumpFunctions()` runs each RPC handler with predefined arguments.
-2. Results land in `__rpc-dump/index.json` (and sharded `__rpc-dump/*.json` files) in the build output.
-3. The static client reads from those files instead of making live RPC calls.
-
-Function names in shard file keys replace `:` with `~` (e.g. `my-plugin:get-data` → `my-plugin~get-data`). Query record maps are embedded directly in `__rpc-dump/index.json`.
-
-#### Static functions
-
-Functions with `type: 'static'` are dumped automatically with no arguments — the recommended default for constant data:
-
-```ts
-const getConfig = defineRpcFunction({
- name: 'my-plugin:get-config',
- type: 'static', // Auto-dumped with inputs: [[]]
- setup: ctx => ({
- handler: async () => ({
- root: ctx.viteConfig.root,
- plugins: ctx.viteConfig.plugins.map(p => p.name),
- }),
- }),
-})
-```
-
-Works in both dev mode (live) and build mode (pre-computed).
-
-#### Query functions with dumps
-
-For `query` functions that need arguments, define `dump` in the setup:
-
-```ts
-const getModule = defineRpcFunction({
- name: 'my-plugin:get-module',
- type: 'query',
- setup: (ctx) => {
- // Collect all module IDs at build time
- const moduleIds = Array.from(ctx.viteServer?.moduleGraph?.idToModuleMap.keys() || [])
-
- return {
- handler: async (id: string) => {
- const module = ctx.viteServer?.moduleGraph?.getModuleById(id)
- return module ? { id, size: module.transformResult?.code.length } : null
- },
- dump: {
- inputs: moduleIds.map(id => [id]), // Pre-compute for all modules
- fallback: null, // Return null for unknown modules
- },
- }
- },
-})
-```
-
-#### Recommendations for plugin authors
-
-For DevTools that work in both dev and build:
-
-1. Prefer `type: 'static'` for functions that return constant data.
-2. Compute context-based data in `setup` rather than accessing global state in handlers.
-3. Define `dump` in `setup` for query functions that need pre-computation.
-4. Provide fallback values so unmatched arguments degrade gracefully.
-
-```ts
-// ✓ Good: returns static data, works in build mode
-const getPluginInfo = defineRpcFunction({
- name: 'my-plugin:info',
- type: 'static',
- setup: ctx => ({
- handler: async () => ({
- version: '1.0.0',
- root: ctx.viteConfig.root,
- }),
- }),
-})
-
-// ✗ Avoid: depends on runtime server state, dev-mode only
-const getLiveMetrics = defineRpcFunction({
- name: 'my-plugin:metrics',
- type: 'query',
- handler: async () => {
- return getCurrentMetrics() // requires live server
- },
-})
-```
-
-`type: 'query'` without a dump still works in dev mode — use it when the data genuinely needs live server state.
-
-### Organization convention
-
-For plugin-scale RPC modules, we recommend:
-
-1. One RPC function per file — small and focused.
-2. `src/node/rpc/index.ts` as the single composition point for registration and type augmentation.
-3. Plugin-specific runtime options stored in `src/node/rpc/context.ts` rather than mutated onto the base DevTools context.
-4. `context.rpc.invokeLocal(...)` for server-side cross-function composition.
-
-Rough file tree:
-
-```text
-src/node/rpc/
-├─ index.ts # exports rpcFunctions + module augmentation
-├─ context.ts # WeakMap-backed helpers (set/get shared rpc context)
-└─ functions/
- ├─ get-info.ts # metadata-style query/static function
- ├─ list-files.ts # list operation, reusable by other functions
- ├─ read-file.ts # can invoke `list-files` via invokeLocal
- └─ write-file.ts # mutation-oriented function
-```
-
-1. `src/node/rpc/index.ts`
-Keep all RPC declarations in one exported list (for example `rpcFunctions`) and centralize type augmentation (`DevToolsRpcServerFunctions`) in the same file.
-
-```ts
-// src/node/rpc/index.ts
-import type { RpcDefinitionsToFunctions } from '@vitejs/devtools-kit'
-import { getInfo } from './functions/get-info'
-import { listFiles } from './functions/list-files'
-import { readFile } from './functions/read-file'
-import '@vitejs/devtools-kit'
-
-export const rpcFunctions = [
- getInfo,
- listFiles,
- readFile,
-] as const // use `as const` to allow type inference
-
-export type ServerFunctions = RpcDefinitionsToFunctions
-
-declare module '@vitejs/devtools-kit' {
- export interface DevToolsRpcServerFunctions extends ServerFunctions {}
-}
-```
-
-2. `src/node/rpc/context.ts`
-Use a shared context helper (for example `WeakMap`-backed `set/get`) to provide plugin-specific options across RPC functions without mutating the base context shape.
-
-```ts
-// src/node/rpc/context.ts
-import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
-
-const rpcContext = new WeakMap()
-
-export function setRpcContext(context: ViteDevToolsNodeContext, options: { targetDir: string }) {
- rpcContext.set(context, options)
-}
-
-export function getRpcContext(context: ViteDevToolsNodeContext) {
- const value = rpcContext.get(context)
- if (!value)
- throw new Error('Missing RPC context')
- return value
-}
-```
-
-```ts
-// plugin setup
-const plugin = {
- devtools: {
- setup(context) {
- setRpcContext(context, { targetDir: 'src' })
- rpcFunctions.forEach(fn => context.rpc.register(fn))
- },
- },
-}
-```
-
-3. `src/node/rpc/functions/read-file.ts`
-For cross-function calls on the server, use `context.rpc.invokeLocal(':list-files')` rather than network-style calls.
-
-```ts
-// src/node/rpc/functions/read-file.ts
-export const readFile = defineRpcFunction({
- name: 'my-plugin:read-file',
- type: 'query',
- dump: async (context) => {
- const files = await context.rpc.invokeLocal('my-plugin:list-files')
- return {
- inputs: files.map(file => [file.path] as [string]),
- }
- },
- setup: () => ({
- handler: async (path: string) => {
- // ...
- },
- }),
-})
-```
-
-The [File Explorer example](/kit/examples#file-explorer) follows these conventions for a plugin with RPC functions and dump support.
-
-## Schema validation
-
-The RPC system supports runtime schema validation through [Valibot](https://valibot.dev). When you provide schemas, TypeScript types are inferred and validation runs at the call site. Schemas are optional — without them, RPC works on plain TypeScript types.
-
-```ts
-import { defineRpcFunction } from '@vitejs/devtools-kit'
-import * as v from 'valibot'
-
-const getModule = defineRpcFunction({
- name: 'my-plugin:get-module',
- type: 'query',
- args: [
- v.string(),
- v.optional(v.object({
- includeSource: v.boolean(),
- })),
- ],
- returns: v.object({
- id: v.string(),
- source: v.optional(v.string()),
- }),
- setup: () => ({
- handler: (id, options) => {
- // Types are automatically inferred from schemas
- // id: string
- // options: { includeSource: boolean } | undefined
- return {
- id,
- source: options?.includeSource ? '...' : undefined,
- }
- },
- }),
-})
-```
-
-## Client-side calls
-
-### In iframe pages
-
-Use `getDevToolsRpcClient()` to get the RPC client:
-
-```ts
-import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
-
-async function loadData() {
- const rpc = await getDevToolsRpcClient()
-
- // Call server function
- const modules = await rpc.call('my-plugin:get-modules')
-
- // With arguments
- const module = await rpc.call('my-plugin:get-module', '/src/main.ts', {
- includeSource: true,
- })
-}
-```
-
-### In action/renderer scripts
-
-Use `ctx.rpc` from the script context:
-
-```ts
-import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'
-
-export default function setup(ctx: DockClientScriptContext) {
- ctx.current.events.on('entry:activated', async () => {
- const data = await ctx.rpc.call('my-plugin:get-modules')
- console.log(data)
- })
-}
-```
-
-### Sharing state across RPC functions
-
-When multiple RPC functions need the same plugin-specific state (a manager instance, plugin options, cached data), key a `WeakMap` by `ViteDevToolsNodeContext`. This keeps the plugin state scoped, garbage-collectable, and out of the base context.
-
-Create a helper file with get/set functions:
-
-```ts
-// src/node/rpc/context.ts
-import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
-
-interface MyPluginContext {
- dataDir: string
- manager: DataManager
-}
-
-const pluginContext = new WeakMap()
-
-export function getPluginContext(ctx: ViteDevToolsNodeContext): MyPluginContext {
- const value = pluginContext.get(ctx)
- if (!value)
- throw new Error('Plugin context not initialized')
- return value
-}
-
-export function setPluginContext(ctx: ViteDevToolsNodeContext, value: MyPluginContext) {
- pluginContext.set(ctx, value)
-}
-```
-
-Initialize the state in your plugin's `devtools.setup`, then access it from any RPC function's `setup`:
-
-::: code-group
-
-```ts [plugin.ts]
-import { rpcFunctions } from './rpc'
-import { setPluginContext } from './rpc/context'
-
-const plugin: Plugin = {
- devtools: {
- setup(ctx) {
- setPluginContext(ctx, {
- dataDir: resolve(ctx.cwd, 'data'),
- manager: new DataManager(),
- })
- rpcFunctions.forEach(fn => ctx.rpc.register(fn))
},
},
}
```
-```ts [functions/get-data.ts]
-import { defineRpcFunction } from '@vitejs/devtools-kit'
-import { getPluginContext } from '../context'
-
-export const getData = defineRpcFunction({
- name: 'my-plugin:get-data',
- type: 'query',
- setup: (ctx) => {
- const { manager } = getPluginContext(ctx)
- return {
- handler: async () => manager.getData(),
- }
- },
-})
-```
-
-:::
+Scope every function id with your package prefix and use kebab-case: `my-plugin:get-modules`.
-### Global client context
+## Function types
-Beyond RPC, the full client context — docks, commands, panel state — is available anywhere in the host page via `getDevToolsClientContext()`. See [Client Script & Context](/kit/client-context).
+| Type | Use for | Cached |
+|------|---------|--------|
+| `query` | Read operations, fetch data | Yes |
+| `static` | Constant data | Indefinitely |
+| `action` | Side effects, mutations | No |
+| `event` | Notifications without a response | No |
-## Client-side functions
-
-The client can also expose functions that the server calls.
-
-### Registering client functions
+## Call from the client
```ts
-import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'
+import { getDevToolsClientContext } from '@vitejs/devtools-kit/client'
-export default function setup(ctx: DockClientScriptContext) {
- ctx.rpc.client.register({
- name: 'my-plugin:highlight-element',
- type: 'action',
- handler: (selector: string) => {
- const el = document.querySelector(selector)
- if (el) {
- el.style.outline = '2px solid red'
- setTimeout(() => {
- el.style.outline = ''
- }, 2000)
- }
- },
- })
-}
-```
-
-### Broadcasting from server
-
-`ctx.rpc.broadcast()` sends an event-style call to every connected client and resolves once dispatch completes:
-
-```ts
-const plugin: Plugin = {
- devtools: {
- setup(ctx) {
- // Later, when you want to notify clients...
- ctx.rpc.broadcast({
- method: 'my-plugin:highlight-element',
- args: ['#app'],
- })
- }
- }
-}
-```
-
-## Type safety
-
-Extend the DevTools Kit interfaces for end-to-end type checking.
-
-### Server functions
-
-```ts
-// src/types.ts
-import '@vitejs/devtools-kit'
-
-declare module '@vitejs/devtools-kit' {
- interface DevToolsRpcServerFunctions {
- 'my-plugin:get-modules': () => Promise
- 'my-plugin:get-module': (
- id: string,
- options?: { includeSource: boolean }
- ) => Promise
- }
-}
-
-interface Module {
- id: string
- size: number
- source?: string
-}
-```
-
-### Client functions
-
-```ts
-// src/types.ts
-declare module '@vitejs/devtools-kit' {
- interface DevToolsRpcClientFunctions {
- 'my-plugin:highlight-element': (selector: string) => void
- 'my-plugin:refresh-ui': () => void
- }
-}
-```
-
-Now TypeScript will autocomplete and validate your RPC calls:
-
-```ts
-// ✓ Type-checked
+const { rpc } = getDevToolsClientContext()!
const modules = await rpc.call('my-plugin:get-modules')
-
-// ✓ Argument types validated
-const module = await rpc.call('my-plugin:get-module', '/src/main.ts')
-
-// ✗ Error: unknown function name
-const data = await rpc.call('my-plugin:unknown')
-```
-
-## Complete example
-
-A plugin with both server and client RPC functions:
-
-::: code-group
-
-```ts [plugin.ts]
-///
-import type { Plugin } from 'vite'
-import { defineRpcFunction } from '@vitejs/devtools-kit'
-
-export default function analyticsPlugin(): Plugin {
- const metrics = new Map()
-
- return {
- name: 'analytics',
-
- transform(code, id) {
- metrics.set(id, code.length)
- },
-
- devtools: {
- setup(ctx) {
- // Server function: get metrics
- ctx.rpc.register(
- defineRpcFunction({
- name: 'analytics:get-metrics',
- type: 'query',
- setup: () => ({
- handler: async () => Object.fromEntries(metrics),
- }),
- })
- )
-
- // Broadcast to clients when metrics change
- ctx.viteServer?.watcher.on('change', (file) => {
- ctx.rpc.broadcast({
- method: 'analytics:metrics-updated',
- args: [file],
- })
- })
- },
- },
- }
-}
-```
-
-```ts [client.ts]
-import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'
-
-export default function setup(ctx: DockClientScriptContext) {
- // Register client function
- ctx.rpc.client.register({
- name: 'analytics:metrics-updated',
- type: 'action',
- handler: (file: string) => {
- console.log('File changed:', file)
- refreshUI()
- },
- })
-
- async function refreshUI() {
- const metrics = await ctx.rpc.call('analytics:get-metrics')
- console.log('Updated metrics:', metrics)
- }
-}
-```
-
-```ts [types.ts]
-import '@vitejs/devtools-kit'
-
-declare module '@vitejs/devtools-kit' {
- interface DevToolsRpcServerFunctions {
- 'analytics:get-metrics': () => Promise>
- }
-
- interface DevToolsRpcClientFunctions {
- 'analytics:metrics-updated': (file: string) => void
- }
-}
```
-:::
+For chunked data — LLM deltas, log lines, build progress, uploads — use [streaming channels](https://devfra.me/guide/streaming) instead of plain functions.
diff --git a/docs/kit/shared-state.md b/docs/kit/shared-state.md
index 5630ce3a3..76d865b4c 100644
--- a/docs/kit/shared-state.md
+++ b/docs/kit/shared-state.md
@@ -4,406 +4,47 @@ outline: deep
# Shared State
-DevTools Kit's shared-state system synchronizes data between server and clients. Changes on either side propagate to every connected party.
+Shared state synchronizes data between the Node side and every connected browser client. A change on either end propagates to all the others, and the value survives reconnects — reach for it instead of RPC when a UI should reactively track server data.
-## Overview
+The full reactive API is in the [Devframe shared-state guide](https://devfra.me/guide/shared-state); this page shows the Vite-plugin usage.
-```mermaid
-flowchart LR
- subgraph ClientA["Client A"]
- A_Value["state.value() { count: 1 }"]
- end
+## Server side
- subgraph Server["Server"]
- S_Mutate["state.mutate() { count: 1 }"]
- end
-
- subgraph ClientB["Client B"]
- B_Value["state.value() { count: 1 }"]
- end
-
- S_Mutate <-->|RPC sync| A_Value
- S_Mutate <-->|RPC sync| B_Value
-```
-
-## Server-side usage
-
-### Creating shared state
-
-`ctx.rpc.sharedState.get()` creates or returns shared state:
+`ctx.rpc.sharedState.get()` creates or returns a keyed state:
```ts
const plugin: Plugin = {
devtools: {
async setup(ctx) {
const state = await ctx.rpc.sharedState.get('my-plugin:state', {
- initialValue: {
- count: 0,
- items: [],
- settings: { theme: 'dark' },
- },
+ initialValue: { count: 0, items: [] as string[] },
})
- // Read current value
- console.log(state.value())
- // => { count: 0, items: [], settings: { theme: 'dark' } }
- }
- }
-}
-```
-
-### Reading state
+ state.value() // read: { count: 0, items: [] }
-```ts
-const state = await ctx.rpc.sharedState.get('my-plugin:state', {
- initialValue: { count: 0 },
-})
-
-// Get current value
-const current = state.value()
-console.log(current.count) // 0
-```
-
-### Mutating state
-
-`state.mutate()` updates the state and syncs the change to every connected client:
-
-```ts
-// Mutate with a function (recommended)
-state.mutate((draft) => {
- draft.count += 1
- draft.items.push({ id: 1, name: 'New item' })
-})
-```
-
-The mutation function receives a mutable draft; changes are batched and synced automatically.
-
-### Example: real-time updates
-
-```ts
-const plugin: Plugin = {
- devtools: {
- async setup(ctx) {
- const state = await ctx.rpc.sharedState.get('my-plugin:state', {
- initialValue: { modules: [], lastUpdate: 0 },
+ state.mutate((draft) => {
+ draft.count++
})
-
- // Update state when Vite processes modules
- ctx.viteServer?.watcher.on('change', (file) => {
- state.mutate((draft) => {
- draft.modules.push(file)
- draft.lastUpdate = Date.now()
- })
- })
- }
- }
-}
-```
-
-## Client-side usage
-
-### Accessing shared state
-
-`client.sharedState.get()` returns the shared state from the client:
-
-```ts
-import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
-
-const client = await getDevToolsRpcClient()
-
-const state = await client.sharedState.get('my-plugin:state')
-
-// Read current value
-console.log(state.value())
-```
-
-The [global client context](/kit/client-context) exposes the same API via `ctx.rpc.sharedState`.
-
-### Subscribing to changes
-
-`state.on('updated', ...)` reacts to state changes:
-
-```ts
-const state = await client.sharedState.get('my-plugin:state')
-
-// Initial value
-console.log(state.value()) // { count: 0 }
-
-// Subscribe to updates
-state.on('updated', (newState) => {
- console.log('State updated:', newState)
- // { count: 1 } - after server mutation
-})
-```
-
-## Framework integration
-
-### Vue
-
-A reactive ref that syncs with shared state:
-
-```ts
-import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
-import { shallowRef } from 'vue'
-
-export async function useSharedState(name: string) {
- const client = await getDevToolsRpcClient()
- const sharedState = await client.sharedState.get(name)
-
- const state = shallowRef(sharedState.value())
-
- sharedState.on('updated', (newState) => {
- state.value = newState
- })
-
- return state
-}
-
-// Usage in component
-const state = await useSharedState('my-plugin:state')
-// `state` is now reactive and auto-updates
-```
-
-### Vue composable (full example)
-
-```vue
-
-
-
-