devtools-plugin-panel
Build devtools panel components that display emitted event data. Listen via EventClient.on(), handle theme (light/dark), use @tanstack/devtools-ui components. Plugin registration (name, render, id, defaultOpen), lifecycle (mount, activate, destroy), max 3 active plugins. Two paths: Solid.js core with devtools-ui for multi-framework support, or framework-specific panels.
Best use case
devtools-plugin-panel is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Build devtools panel components that display emitted event data. Listen via EventClient.on(), handle theme (light/dark), use @tanstack/devtools-ui components. Plugin registration (name, render, id, defaultOpen), lifecycle (mount, activate, destroy), max 3 active plugins. Two paths: Solid.js core with devtools-ui for multi-framework support, or framework-specific panels.
Teams using devtools-plugin-panel should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/devtools-plugin-panel/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How devtools-plugin-panel Compares
| Feature / Agent | devtools-plugin-panel | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Build devtools panel components that display emitted event data. Listen via EventClient.on(), handle theme (light/dark), use @tanstack/devtools-ui components. Plugin registration (name, render, id, defaultOpen), lifecycle (mount, activate, destroy), max 3 active plugins. Two paths: Solid.js core with devtools-ui for multi-framework support, or framework-specific panels.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
Related Guides
SKILL.md Source
## TanStackDevtoolsPlugin Interface
The low-level contract every plugin implements. Framework adapters wrap this automatically.
```ts
// Source: packages/devtools/src/context/devtools-context.tsx
interface TanStackDevtoolsPlugin {
id?: string
name: string | ((el: HTMLHeadingElement, theme: 'dark' | 'light') => void)
render: (el: HTMLDivElement, theme: 'dark' | 'light') => void
destroy?: (pluginId: string) => void
defaultOpen?: boolean
}
```
- **`name`** (required) -- String tab title, or function receiving `(el, theme)` for custom rendering.
- **`render`** (required) -- Called on activation with a `<div>` container and theme. Called again on theme change.
- **`id`** (optional) -- Stable identifier. If omitted: `name.toLowerCase().replace(' ', '-')-{index}`. Explicit ids persist selection across reloads.
- **`defaultOpen`** (optional) -- Opens panel on first load when no saved state. Max 3 open. Does not override saved preferences.
- **`destroy`** (optional) -- Called on deactivation or unmount. Framework adapters handle cleanup automatically.
---
## Two Development Paths
### Path 1: Solid.js Core + Framework Adapters (Multi-Framework)
Build the panel in Solid.js using `@tanstack/devtools-ui` components. Use `constructCoreClass` for lazy loading, then `createReactPanel`/`createSolidPanel` to wrap for each framework. The devtools core is Solid, so Solid panels run natively.
### Path 2: Framework-Specific Panel (Single Framework)
Build directly in your framework and use `createReactPlugin`/`createVuePlugin`/`createSolidPlugin`/`createPreactPlugin` from `@tanstack/devtools-utils`.
---
## Path 1: Solid.js Core Panel
### Step 1: Define Event Map and Create EventClient
```ts
// src/event-client.ts
import { EventClient } from '@tanstack/devtools-event-client'
type StoreEvents = {
'state-changed': { storeName: string; state: unknown; timestamp: number }
'action-dispatched': { storeName: string; action: string; payload: unknown }
reset: void
}
class StoreInspectorClient extends EventClient<StoreEvents> {
constructor() {
super({ pluginId: 'store-inspector' })
}
}
export const storeInspector = new StoreInspectorClient()
```
Event names are suffixes only. The `pluginId` is prepended automatically: `'store-inspector:state-changed'`.
### Step 2: Build the Solid.js Panel Component
```tsx
/** @jsxImportSource solid-js */
import { createSignal, onCleanup, For } from 'solid-js'
import {
MainPanel,
Header,
HeaderLogo,
Section,
SectionTitle,
JsonTree,
Button,
Tag,
useTheme,
} from '@tanstack/devtools-ui'
import { storeInspector } from './event-client'
export default function StoreInspectorPanel() {
const { theme } = useTheme()
const [state, setState] = createSignal<Record<string, unknown>>({})
const [actions, setActions] = createSignal<
Array<{ action: string; payload: unknown }>
>([])
const cleanupState = storeInspector.on('state-changed', (e) => {
setState((prev) => ({ ...prev, [e.payload.storeName]: e.payload.state }))
})
const cleanupActions = storeInspector.on('action-dispatched', (e) => {
setActions((prev) => [
...prev,
{ action: e.payload.action, payload: e.payload.payload },
])
})
onCleanup(() => {
cleanupState()
cleanupActions()
})
return (
<MainPanel>
<Header>
<HeaderLogo flavor={{ light: '#1a1a2e', dark: '#e0e0e0' }}>
Store Inspector
</HeaderLogo>
</Header>
<Section>
<SectionTitle>Current State</SectionTitle>
<JsonTree value={state()} copyable defaultExpansionDepth={2} />
</Section>
<Section>
<SectionTitle>
Action Log
<Tag color="purple" label="Actions" count={actions().length} />
</SectionTitle>
<For each={actions()}>
{(a) => (
<div>
<strong>{a.action}</strong>
<JsonTree value={a.payload} copyable defaultExpansionDepth={1} />
</div>
)}
</For>
<Button variant="danger" onClick={() => setActions([])}>
Clear Log
</Button>
</Section>
</MainPanel>
)
}
```
### Step 3: Create Core Class and Framework Adapters
```ts
// src/core.ts
import { constructCoreClass } from '@tanstack/devtools-utils/solid/class'
export const [StoreInspectorCore, NoOpStoreInspectorCore] = constructCoreClass(
() => import('./panel'),
)
```
```tsx
// src/react.tsx
import { createReactPanel } from '@tanstack/devtools-utils/react'
import { StoreInspectorCore } from './core'
export const [StoreInspectorPanel, NoOpStoreInspectorPanel] =
createReactPanel(StoreInspectorCore)
```
```tsx
// src/react-plugin.tsx
import { createReactPlugin } from '@tanstack/devtools-utils/react'
import { StoreInspectorPanel } from './react'
export const [StoreInspectorPlugin, NoOpStoreInspectorPlugin] =
createReactPlugin({
name: 'Store Inspector',
id: 'store-inspector',
defaultOpen: true,
Component: StoreInspectorPanel,
})
```
### Step 4: Register
```tsx
import { TanStackDevtools } from '@tanstack/react-devtools'
import { StoreInspectorPlugin } from 'your-package/react-plugin'
function App() {
return (
<>
<YourApp />
<TanStackDevtools plugins={[StoreInspectorPlugin()]} />
</>
)
}
```
---
## Path 2: Framework-Specific Panel (React Example)
```tsx
import { useState, useEffect } from 'react'
import { EventClient } from '@tanstack/devtools-event-client'
import { createReactPlugin } from '@tanstack/devtools-utils/react'
type MyEvents = {
'data-update': { items: Array<{ id: string; value: number }> }
}
class MyPluginClient extends EventClient<MyEvents> {
constructor() {
super({ pluginId: 'my-plugin' })
}
}
export const myPlugin = new MyPluginClient()
function MyPluginPanel({ theme }: { theme?: 'light' | 'dark' }) {
const [items, setItems] = useState<Array<{ id: string; value: number }>>([])
useEffect(() => {
const cleanup = myPlugin.on('data-update', (e) => {
setItems(e.payload.items)
})
return cleanup
}, [])
return (
<div style={{ color: theme === 'dark' ? '#fff' : '#000' }}>
<h3>My Plugin</h3>
<ul>
{items.map((item) => (
<li key={item.id}>
{item.id}: {item.value}
</li>
))}
</ul>
</div>
)
}
export const [MyPlugin, NoOpMyPlugin] = createReactPlugin({
name: 'My Plugin',
id: 'my-plugin',
defaultOpen: false,
Component: MyPluginPanel,
})
```
---
## Plugin Lifecycle Sequence
1. **Initialization** -- `TanStackDevtoolsCore` receives `plugins` array. Each plugin gets an `id` (explicit or generated).
2. **DOM containers created** -- Core creates `<div id="plugin-container-{id}">` and `<h3 id="plugin-title-container-{id}">` per plugin.
3. **Activation** -- On tab click or `defaultOpen`, `plugin.render(container, theme)` called.
4. **Framework portaling** -- React uses `createPortal`, Solid uses `<Portal>`, Vue uses `<Teleport>`.
5. **Theme change** -- `render` called again with new theme value.
6. **Deactivation/Unmount** -- `destroy(pluginId)` called if provided. Framework adapters handle cleanup.
Active plugin selection persisted in `localStorage` under key `tanstack_devtools_state`.
---
## Common Mistakes
### CRITICAL: Not Cleaning Up Event Listeners
Each `on()` returns a cleanup function. Forgetting it causes memory leaks and duplicate handlers.
Wrong:
```ts
useEffect(() => {
client.on('state', cb)
}, [])
```
Correct:
```ts
useEffect(() => {
const cleanup = client.on('state', cb)
return cleanup
}, [])
```
In Solid, use `onCleanup()`:
```ts
const cleanup = storeInspector.on('state-changed', handler)
onCleanup(cleanup)
```
Source: docs/building-custom-plugins.md
### HIGH: Oversubscribing to Events in Multiple Components
Do not call `on()` in multiple components for the same event. Subscribe once in a shared store/hook.
Wrong:
```ts
function ComponentA() {
useEffect(() => {
const c = client.on('state', cb1)
return c
}, [])
}
function ComponentB() {
useEffect(() => {
const c = client.on('state', cb2)
return c
}, [])
}
```
Correct:
```ts
function useStoreState() {
const [state, setState] = useState(null)
useEffect(() => {
const cleanup = client.on('state', (e) => setState(e.payload))
return cleanup
}, [])
return state
}
```
Source: maintainer interview
### MEDIUM: Hardcoding Repeated Event Payload Fields
When emitting events that share common fields, create a shared base object.
Wrong:
```ts
client.emit('state-changed', { storeName: 'main', version: '1.0', state })
client.emit('action-dispatched', { storeName: 'main', version: '1.0', action })
```
Correct:
```ts
const base = { storeName: 'main', version: '1.0' }
client.emit('state-changed', { ...base, state })
client.emit('action-dispatched', { ...base, action })
```
Source: maintainer interview
### MEDIUM: Ignoring Theme Prop in Panel Component
Panels must adapt styling to theme. Factory-created plugins receive `props.theme`.
Wrong:
```tsx
function MyPanel() {
return <div style={{ color: 'white' }}>Always white text</div>
}
```
Correct:
```tsx
function MyPanel({ theme }: { theme?: 'light' | 'dark' }) {
return (
<div style={{ color: theme === 'dark' ? '#e0e0e0' : '#1a1a1a' }}>
Theme-aware text
</div>
)
}
```
In Solid panels using devtools-ui, use `useTheme()` instead of prop drilling.
Source: docs/plugin-lifecycle.md
### MEDIUM: Not Knowing Max 3 Active Plugins Limit
`MAX_ACTIVE_PLUGINS = 3` (in `packages/devtools/src/utils/constants.ts`). If more than 3 set `defaultOpen: true`, only the first 3 open. Activating a 4th deactivates the earliest. Single-plugin exception: if only 1 plugin is registered, it opens automatically.
Source: packages/devtools/src/utils/get-default-active-plugins.ts
### MEDIUM: Using Raw DOM Manipulation Instead of Framework Portals
Framework adapters handle portaling. Do not manually manipulate DOM.
Wrong:
```ts
render: (el) => {
const div = document.createElement('div')
div.textContent = 'Hello'
el.appendChild(div)
}
```
Correct:
```tsx
import { createReactPlugin } from '@tanstack/devtools-utils/react'
const [Plugin, NoOpPlugin] = createReactPlugin({
name: 'My Plugin',
Component: ({ theme }) => <div>Hello</div>,
})
```
Source: docs/plugin-lifecycle.md
### MEDIUM: Not Keeping Devtools Packages at Latest Versions
All `@tanstack/devtools-*` packages should be on compatible versions. For external plugins, pin to compatible ranges.
Source: maintainer interview
## References
- [devtools-ui components and API](references/panel-api.md)Related Skills
devtools-instrumentation
Analyze library codebase for critical architecture and debugging points, add strategic event emissions. Identify middleware boundaries, state transitions, lifecycle hooks. Consolidate events (1 not 15), debounce high-frequency updates, DRY shared payload fields, guard emit() for production. Transparent server/client event bridging.
devtools-event-client
Create typed EventClient for a library. Define event maps with typed payloads, pluginId auto-prepend namespacing, emit()/on()/onAll()/onAllPluginEvents() API. Connection lifecycle (5 retries, 300ms), event queuing, enabled/disabled state, SSR fallbacks, singleton pattern. Unique pluginId requirement to avoid event collisions.
devtools-bidirectional
Two-way event patterns between devtools panel and application. App-to-devtools observation, devtools-to-app commands, time-travel debugging with snapshots and revert. structuredClone for snapshot safety, distinct event suffixes for observation vs commands, serializable payloads only.
devtools-production
Handle devtools in production vs development. removeDevtoolsOnBuild, devDependency vs regular dependency, conditional imports, NoOp plugin variants for tree-shaking, non-Vite production exclusion patterns.
devtools-marketplace
Publish plugin to npm and submit to TanStack Devtools Marketplace. PluginMetadata registry format, plugin-registry.ts, pluginImport (importName, type), requires (packageName, minVersion), framework tagging, multi-framework submissions, featured plugins.
devtools-app-setup
Install TanStack Devtools, pick framework adapter (React/Vue/Solid/Preact), register plugins via plugins prop, configure shell (position, hotkeys, theme, hideUntilHover, requireUrlFlag, eventBusConfig). TanStackDevtools component, defaultOpen, localStorage persistence.
devtools-vite-plugin
Configure @tanstack/devtools-vite for source inspection (data-tsd-source, inspectHotkey, ignore patterns), console piping (client-to-server, server-to-client, levels), enhanced logging, server event bus (port, host, HTTPS), production stripping (removeDevtoolsOnBuild), editor integration (launch-editor, custom editor.open). Must be FIRST plugin in Vite config. Vite ^6 || ^7 only.
devtools-framework-adapters
Use devtools-utils factory functions to create per-framework plugin adapters. createReactPlugin/createSolidPlugin/createVuePlugin/createPreactPlugin, createReactPanel/createSolidPanel/createVuePanel/createPreactPanel. [Plugin, NoOpPlugin] tuple for tree-shaking. DevtoolsPanelProps (theme). Vue uses (name, component) not options object. Solid render must be function.
laravel-plugin-discovery
Discover and evaluate Laravel packages via LaraPlugins.io MCP. Use when the user wants to find plugins, check package health, or assess Laravel/PHP compatibility.
mixpanel-automation
Automate Mixpanel tasks via Rube MCP (Composio): events, segmentation, funnels, cohorts, user profiles, JQL queries. Always search tools first for current schemas.
Teleton Plugin Builder
This skill guides an AI agent through the process of building custom plugins for Teleton, a Telegram AI agent operating on the TON blockchain.
claude-code-plugin-release
Automated semantic versioning and release workflow for Claude Code plugins. Handles version increments across package.json, marketplace.json, and plugin.json, build verification, git tagging, GitHub releases, and changelog generation.