Skip to content

Commit 599db24

Browse files
committed
feat: commands system
1 parent f0c87b1 commit 599db24

25 files changed

Lines changed: 1933 additions & 132 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const DevToolsKitNav = [
1414
{ text: 'Dock System', link: '/kit/dock-system' },
1515
{ text: 'RPC', link: '/kit/rpc' },
1616
{ text: 'Shared State', link: '/kit/shared-state' },
17+
{ text: 'Commands', link: '/kit/commands' },
1718
{ text: 'Logs & Notifications', link: '/kit/logs' },
1819
{ text: 'Terminals & Processes', link: '/kit/terminals' },
1920
{ text: 'Examples', link: '/kit/examples' },
@@ -69,6 +70,7 @@ export default extendConfig(withMermaid(defineConfig({
6970
{ text: 'Dock System', link: '/kit/dock-system' },
7071
{ text: 'RPC', link: '/kit/rpc' },
7172
{ text: 'Shared State', link: '/kit/shared-state' },
73+
{ text: 'Commands', link: '/kit/commands' },
7274
{ text: 'Logs', link: '/kit/logs' },
7375
{ text: 'JSON Render', link: '/kit/json-render' },
7476
{ text: 'Terminals', link: '/kit/terminals' },

docs/kit/commands.md

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# Commands & Command Palette
6+
7+
DevTools Kit provides a commands system that lets plugins register executable commands — both on the server and client side. Users can discover and run commands through a built-in command palette, and customize keyboard shortcuts.
8+
9+
## Overview
10+
11+
```mermaid
12+
sequenceDiagram
13+
participant Plugin as Vite Plugin (Server)
14+
participant Client as Browser Client
15+
participant Palette as Command Palette
16+
17+
Plugin->>Plugin: ctx.commands.register({ id, handler })
18+
Note over Plugin: Commands synced via shared state
19+
Plugin-->>Client: Server commands available
20+
21+
Client->>Client: register({ id, action })
22+
Note over Client: Client commands registered locally
23+
24+
Palette->>Palette: Merge server + client commands
25+
Palette->>Client: User selects command
26+
Client->>Plugin: RPC call (server commands)
27+
Client->>Client: Direct action (client commands)
28+
```
29+
30+
## Server-Side Commands
31+
32+
### Defining Commands
33+
34+
Use `defineCommand` and register via `ctx.commands.register()`:
35+
36+
```ts
37+
import { defineCommand } from '@vitejs/devtools-kit'
38+
39+
const clearCache = defineCommand({
40+
id: 'my-plugin:clear-cache',
41+
title: 'Clear Build Cache',
42+
description: 'Remove all cached build artifacts',
43+
icon: 'ph:trash-duotone',
44+
category: 'tools',
45+
handler: async () => {
46+
await fs.rm('.cache', { recursive: true })
47+
},
48+
})
49+
```
50+
51+
Register it in your plugin setup:
52+
53+
```ts
54+
const plugin: Plugin = {
55+
devtools: {
56+
setup(ctx) {
57+
ctx.commands.register(clearCache)
58+
},
59+
},
60+
}
61+
```
62+
63+
### Command Options
64+
65+
| Field | Type | Description |
66+
|-------|------|-------------|
67+
| `id` | `string` | **Required.** Unique namespaced ID (e.g. `my-plugin:action`) |
68+
| `title` | `string` | **Required.** Human-readable title shown in the palette |
69+
| `description` | `string` | Optional description text |
70+
| `icon` | `string` | Iconify icon string (e.g. `ph:trash-duotone`) |
71+
| `category` | `string` | Category for grouping |
72+
| `showInPalette` | `boolean` | Whether to show in command palette (default: `true`) |
73+
| `keybindings` | `DevToolsCommandKeybinding[]` | Default keyboard shortcuts |
74+
| `handler` | `Function` | Server-side handler. Optional if the command is a group for children. |
75+
| `children` | `DevToolsServerCommandInput[]` | Static sub-commands (two levels max) |
76+
77+
### Command Handle
78+
79+
`register()` returns a handle for live updates:
80+
81+
```ts
82+
const handle = ctx.commands.register({
83+
id: 'my-plugin:status',
84+
title: 'Show Status',
85+
handler: () => { /* ... */ },
86+
})
87+
88+
// Update later
89+
handle.update({ title: 'Show Status (3 items)' })
90+
91+
// Remove
92+
handle.unregister()
93+
```
94+
95+
## Sub-Commands
96+
97+
Commands can have static children, creating a two-level hierarchy. In the palette, selecting a parent drills down into its children.
98+
99+
```ts
100+
ctx.commands.register({
101+
id: 'git',
102+
title: 'Git',
103+
icon: 'ph:git-branch-duotone',
104+
category: 'tools',
105+
// No handler — group-only parent
106+
children: [
107+
{
108+
id: 'git:commit',
109+
title: 'Commit',
110+
icon: 'ph:check-duotone',
111+
keybindings: [{ key: 'Mod+Shift+G' }],
112+
handler: async () => { /* ... */ },
113+
},
114+
{
115+
id: 'git:push',
116+
title: 'Push',
117+
handler: async () => { /* ... */ },
118+
},
119+
{
120+
id: 'git:pull',
121+
title: 'Pull',
122+
handler: async () => { /* ... */ },
123+
},
124+
],
125+
})
126+
```
127+
128+
In the palette, users see **Git** → select it → drill down to see **Commit**, **Push**, **Pull**. Sub-commands with keybindings (like `Mod+Shift+G` above) can be executed directly via the shortcut without opening the palette.
129+
130+
> [!NOTE]
131+
> Each child must have a globally unique `id`. We recommend the pattern `parentId:childAction` (e.g. `git:commit`).
132+
133+
## Keyboard Shortcuts
134+
135+
### Defining Shortcuts
136+
137+
Add default keybindings when registering a command:
138+
139+
```ts
140+
ctx.commands.register({
141+
id: 'my-plugin:toggle-overlay',
142+
title: 'Toggle Overlay',
143+
keybindings: [
144+
{ key: 'Mod+Shift+O' },
145+
],
146+
handler: () => { /* ... */ },
147+
})
148+
```
149+
150+
### Key Format
151+
152+
Use `Mod` as a platform-aware modifier — it maps to `Cmd` on macOS and `Ctrl` on other platforms.
153+
154+
| Key string | macOS | Windows/Linux |
155+
|------------|-------|---------------|
156+
| `Mod+K` | `Cmd+K` | `Ctrl+K` |
157+
| `Mod+Shift+P` | `Cmd+Shift+P` | `Ctrl+Shift+P` |
158+
| `Alt+N` | `Option+N` | `Alt+N` |
159+
160+
### Conditional Shortcuts
161+
162+
Use the `when` field for shortcuts that only activate in certain contexts:
163+
164+
```ts
165+
keybindings: [
166+
{ key: 'Mod+Shift+D', when: 'clientType == embedded' },
167+
]
168+
```
169+
170+
Supported context variables:
171+
172+
| Variable | Type | Description |
173+
|----------|------|-------------|
174+
| `clientType` | `'embedded' \| 'standalone'` | Current client mode |
175+
| `dockOpen` | `boolean` | Whether the dock panel is open |
176+
| `paletteOpen` | `boolean` | Whether the command palette is open |
177+
178+
Supported operators: `==`, `!=`, `&&`, `||`, `!` (negation), bare truthy checks.
179+
180+
### User Overrides
181+
182+
Users can customize shortcuts in the DevTools Settings page under **Keyboard Shortcuts**. Overrides are stored in shared state and persist across sessions. Setting an empty array disables a shortcut.
183+
184+
## Command Palette
185+
186+
The built-in command palette is toggled with `Mod+K` (or `Ctrl+K` on Windows/Linux). It provides:
187+
188+
- **Fuzzy search** across all registered commands (including sub-commands)
189+
- **Keyboard navigation** — Arrow keys to navigate, Enter to select, Escape to close
190+
- **Drill-down** — Commands with children show a breadcrumb navigation
191+
- **Server command execution** — Server commands are executed via RPC with a loading indicator
192+
- **Dynamic sub-menus** — Client commands can return sub-items at runtime
193+
194+
### Embedded vs Standalone
195+
196+
- **Embedded mode**: The palette floats over the user's application as part of the DevTools overlay
197+
- **Standalone mode**: The palette appears as a modal dialog in the standalone DevTools window
198+
199+
## Client-Side Commands
200+
201+
Client commands are registered in the webcomponent context and execute directly in the browser:
202+
203+
```ts
204+
// From within the DevTools client context
205+
context.commands.register({
206+
id: 'devtools:theme',
207+
source: 'client',
208+
title: 'Theme',
209+
icon: 'ph:palette-duotone',
210+
children: [
211+
{
212+
id: 'devtools:theme:light',
213+
source: 'client',
214+
title: 'Light',
215+
action: () => setTheme('light'),
216+
},
217+
{
218+
id: 'devtools:theme:dark',
219+
source: 'client',
220+
title: 'Dark',
221+
action: () => setTheme('dark'),
222+
},
223+
],
224+
})
225+
```
226+
227+
Client commands can also return dynamic sub-items:
228+
229+
```ts
230+
context.commands.register({
231+
id: 'devtools:docs',
232+
source: 'client',
233+
title: 'Documentation',
234+
action: async () => {
235+
const docs = await fetchDocs()
236+
return docs.map(doc => ({
237+
id: `docs:${doc.slug}`,
238+
source: 'client' as const,
239+
title: doc.title,
240+
action: () => window.open(doc.url, '_blank'),
241+
}))
242+
},
243+
})
244+
```
245+
246+
## Complete Example
247+
248+
::: code-group
249+
250+
```ts [plugin.ts]
251+
/// <reference types="@vitejs/devtools-kit" />
252+
import type { Plugin } from 'vite'
253+
import { defineCommand } from '@vitejs/devtools-kit'
254+
255+
export default function myPlugin(): Plugin {
256+
return {
257+
name: 'my-plugin',
258+
259+
devtools: {
260+
setup(ctx) {
261+
// Simple command
262+
ctx.commands.register(defineCommand({
263+
id: 'my-plugin:restart',
264+
title: 'Restart Dev Server',
265+
icon: 'ph:arrow-clockwise-duotone',
266+
keybindings: [{ key: 'Mod+Shift+R' }],
267+
handler: async () => {
268+
await ctx.viteServer?.restart()
269+
},
270+
}))
271+
272+
// Command with sub-commands
273+
ctx.commands.register(defineCommand({
274+
id: 'my-plugin:cache',
275+
title: 'Cache',
276+
icon: 'ph:database-duotone',
277+
children: [
278+
{
279+
id: 'my-plugin:cache:clear',
280+
title: 'Clear Cache',
281+
handler: async () => { /* ... */ },
282+
},
283+
{
284+
id: 'my-plugin:cache:inspect',
285+
title: 'Inspect Cache',
286+
handler: async () => { /* ... */ },
287+
},
288+
],
289+
}))
290+
},
291+
},
292+
}
293+
}
294+
```
295+
296+
:::

packages/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"@xterm/addon-fit": "catalog:frontend",
8181
"@xterm/xterm": "catalog:frontend",
8282
"dompurify": "catalog:frontend",
83+
"fuse.js": "catalog:frontend",
8384
"human-id": "catalog:inlined",
8485
"tsdown": "catalog:build",
8586
"typescript": "catalog:devtools",
@@ -100,6 +101,7 @@
100101
"@xterm/xterm": "6.0.0",
101102
"ansis": "4.2.0",
102103
"dompurify": "3.3.3",
104+
"fuse.js": "7.1.0",
103105
"get-port-please": "3.2.0",
104106
"sisteransi": "1.0.5",
105107
"zod": "4.3.6"

packages/core/src/client/webcomponents/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)