Skip to content

Commit 210d826

Browse files
authored
feat: commands system, commands palette, shortcuts and when clauses (#247)
1 parent 22621ca commit 210d826

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+3404
-485
lines changed

alias.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const alias = {
1717
'@vitejs/devtools-kit/utils/events': r('kit/src/utils/events.ts'),
1818
'@vitejs/devtools-kit/utils/nanoid': r('kit/src/utils/nanoid.ts'),
1919
'@vitejs/devtools-kit/utils/human-id': r('kit/src/utils/human-id.ts'),
20+
'@vitejs/devtools-kit/utils/when': r('kit/src/utils/when.ts'),
2021
'@vitejs/devtools-kit/utils/shared-state': r('kit/src/utils/shared-state.ts'),
2122
'@vitejs/devtools-kit': r('kit/src/index.ts'),
2223
'@vitejs/devtools-rolldown': r('rolldown/src/index.ts'),

docs/.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ 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' },
18+
{ text: 'When Clauses', link: '/kit/when-clauses' },
1719
{ text: 'Logs & Notifications', link: '/kit/logs' },
1820
{ text: 'Terminals & Processes', link: '/kit/terminals' },
1921
{ text: 'Examples', link: '/kit/examples' },
@@ -69,6 +71,8 @@ export default extendConfig(withMermaid(defineConfig({
6971
{ text: 'Dock System', link: '/kit/dock-system' },
7072
{ text: 'RPC', link: '/kit/rpc' },
7173
{ text: 'Shared State', link: '/kit/shared-state' },
74+
{ text: 'Commands', link: '/kit/commands' },
75+
{ text: 'When Clauses', link: '/kit/when-clauses' },
7276
{ text: 'Logs', link: '/kit/logs' },
7377
{ text: 'JSON Render', link: '/kit/json-render' },
7478
{ text: 'Terminals', link: '/kit/terminals' },

docs/kit/commands.md

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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 \| '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. |
73+
| `when` | `string` | Conditional visibility expression (see [When Clauses](/kit/when-clauses)) |
74+
| `keybindings` | `DevToolsCommandKeybinding[]` | Default keyboard shortcuts |
75+
| `handler` | `Function` | Server-side handler. Optional if the command is a group for children. |
76+
| `children` | `DevToolsServerCommandInput[]` | Static sub-commands (two levels max) |
77+
78+
### Command Handle
79+
80+
`register()` returns a handle for live updates:
81+
82+
```ts
83+
const handle = ctx.commands.register({
84+
id: 'my-plugin:status',
85+
title: 'Show Status',
86+
handler: () => { /* ... */ },
87+
})
88+
89+
// Update later
90+
handle.update({ title: 'Show Status (3 items)' })
91+
92+
// Remove
93+
handle.unregister()
94+
```
95+
96+
## Sub-Commands
97+
98+
Commands can have static children, creating a two-level hierarchy. In the palette, selecting a parent drills down into its children.
99+
100+
```ts
101+
ctx.commands.register({
102+
id: 'git',
103+
title: 'Git',
104+
icon: 'ph:git-branch-duotone',
105+
category: 'tools',
106+
// No handler — group-only parent
107+
children: [
108+
{
109+
id: 'git:commit',
110+
title: 'Commit',
111+
icon: 'ph:check-duotone',
112+
keybindings: [{ key: 'Mod+Shift+G' }],
113+
handler: async () => { /* ... */ },
114+
},
115+
{
116+
id: 'git:push',
117+
title: 'Push',
118+
handler: async () => { /* ... */ },
119+
},
120+
{
121+
id: 'git:pull',
122+
title: 'Pull',
123+
handler: async () => { /* ... */ },
124+
},
125+
],
126+
})
127+
```
128+
129+
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.
130+
131+
> [!NOTE]
132+
> Each child must have a globally unique `id`. We recommend the pattern `parentId:childAction` (e.g. `git:commit`).
133+
134+
## Keyboard Shortcuts
135+
136+
### Defining Shortcuts
137+
138+
Add default keybindings when registering a command:
139+
140+
```ts
141+
ctx.commands.register({
142+
id: 'my-plugin:toggle-overlay',
143+
title: 'Toggle Overlay',
144+
keybindings: [
145+
{ key: 'Mod+Shift+O' },
146+
],
147+
handler: () => { /* ... */ },
148+
})
149+
```
150+
151+
### Key Format
152+
153+
Use `Mod` as a platform-aware modifier — it maps to `Cmd` on macOS and `Ctrl` on other platforms.
154+
155+
| Key string | macOS | Windows/Linux |
156+
|------------|-------|---------------|
157+
| `Mod+K` | `Cmd+K` | `Ctrl+K` |
158+
| `Mod+Shift+P` | `Cmd+Shift+P` | `Ctrl+Shift+P` |
159+
| `Alt+N` | `Option+N` | `Alt+N` |
160+
161+
### Conditional `when` Clauses
162+
163+
Commands support a `when` expression for conditional visibility and activation:
164+
165+
```ts
166+
ctx.commands.register(defineCommand({
167+
id: 'my-plugin:embedded-only',
168+
title: 'Embedded-Only Action',
169+
when: 'clientType == embedded',
170+
handler: async () => { /* ... */ },
171+
}))
172+
```
173+
174+
When set, the command is only shown in the palette and only triggerable via shortcuts when the expression evaluates to `true`. Supports `==`, `!=`, `&&`, `||`, `!`, bare truthy, literal `true`/`false`, and namespaced keys like `vite.mode`.
175+
176+
See [When Clauses](/kit/when-clauses) for the full syntax reference, context variables, and namespaced key support.
177+
178+
### User Overrides
179+
180+
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.
181+
182+
### Shortcut Editor
183+
184+
The Settings page includes an inline shortcut editor with:
185+
186+
- **Key capture** — click the input and press any key combination
187+
- **Modifier toggles** — toggle Cmd/Ctrl, Alt, Shift individually
188+
- **Conflict detection** — warns when a shortcut conflicts with:
189+
- Common browser shortcuts (e.g. `Cmd+T` → "Open new tab", `Cmd+W` → "Close tab")
190+
- Other registered commands
191+
- Weak shortcuts (single key without modifiers)
192+
193+
The list of known browser shortcuts (`KNOWN_BROWSER_SHORTCUTS`) is exported from `@vitejs/devtools-kit` and maps each key combination to a human-readable description.
194+
195+
## Command Palette
196+
197+
The built-in command palette is toggled with `Mod+K` (or `Ctrl+K` on Windows/Linux). It provides:
198+
199+
- **Fuzzy search** across all registered commands (including sub-commands)
200+
- **Keyboard navigation** — Arrow keys to navigate, Enter to select, Escape to close
201+
- **Drill-down** — Commands with children show a breadcrumb navigation
202+
- **Server command execution** — Server commands are executed via RPC with a loading indicator
203+
- **Dynamic sub-menus** — Client commands can return sub-items at runtime
204+
205+
### Embedded vs Standalone
206+
207+
- **Embedded mode**: The palette floats over the user's application as part of the DevTools overlay
208+
- **Standalone mode**: The palette appears as a modal dialog in the standalone DevTools window
209+
210+
## Client-Side Commands
211+
212+
Client commands are registered in the webcomponent context and execute directly in the browser:
213+
214+
```ts
215+
// From within the DevTools client context
216+
context.commands.register({
217+
id: 'devtools:theme',
218+
source: 'client',
219+
title: 'Theme',
220+
icon: 'ph:palette-duotone',
221+
children: [
222+
{
223+
id: 'devtools:theme:light',
224+
source: 'client',
225+
title: 'Light',
226+
action: () => setTheme('light'),
227+
},
228+
{
229+
id: 'devtools:theme:dark',
230+
source: 'client',
231+
title: 'Dark',
232+
action: () => setTheme('dark'),
233+
},
234+
],
235+
})
236+
```
237+
238+
Client commands can also return dynamic sub-items:
239+
240+
```ts
241+
context.commands.register({
242+
id: 'devtools:docs',
243+
source: 'client',
244+
title: 'Documentation',
245+
action: async () => {
246+
const docs = await fetchDocs()
247+
return docs.map(doc => ({
248+
id: `docs:${doc.slug}`,
249+
source: 'client' as const,
250+
title: doc.title,
251+
action: () => window.open(doc.url, '_blank'),
252+
}))
253+
},
254+
})
255+
```
256+
257+
## Complete Example
258+
259+
::: code-group
260+
261+
```ts [plugin.ts]
262+
/// <reference types="@vitejs/devtools-kit" />
263+
import type { Plugin } from 'vite'
264+
import { defineCommand } from '@vitejs/devtools-kit'
265+
266+
export default function myPlugin(): Plugin {
267+
return {
268+
name: 'my-plugin',
269+
270+
devtools: {
271+
setup(ctx) {
272+
// Simple command
273+
ctx.commands.register(defineCommand({
274+
id: 'my-plugin:restart',
275+
title: 'Restart Dev Server',
276+
icon: 'ph:arrow-clockwise-duotone',
277+
keybindings: [{ key: 'Mod+Shift+R' }],
278+
handler: async () => {
279+
await ctx.viteServer?.restart()
280+
},
281+
}))
282+
283+
// Command with sub-commands
284+
ctx.commands.register(defineCommand({
285+
id: 'my-plugin:cache',
286+
title: 'Cache',
287+
icon: 'ph:database-duotone',
288+
children: [
289+
{
290+
id: 'my-plugin:cache:clear',
291+
title: 'Clear Cache',
292+
handler: async () => { /* ... */ },
293+
},
294+
{
295+
id: 'my-plugin:cache:inspect',
296+
title: 'Inspect Cache',
297+
handler: async () => { /* ... */ },
298+
},
299+
],
300+
}))
301+
},
302+
},
303+
}
304+
}
305+
```
306+
307+
:::

0 commit comments

Comments
 (0)