VSCode DeckA Stream Deck–style button grid for VSCode. Configure buttons in a JSON file, click them to run VSCode commands or shell commands — singly or as sequential chains. Works in a sidebar panel or a floating window. Designed as a single place to collect the commands you run the most: Features
InstallationFrom the VS Code Marketplace — search for VSCode Deck in the Extensions sidebar ( From source — clone, build, and launch the Extension Development Host:
Open the folder in VSCode and press F5 — that launches an Extension Development Host window with the extension loaded. The Deck icon (a grid) appears in the Activity Bar. As a
That produces a
Getting started
The config file is watched — saves are picked up instantly, no reload required. ConfigurationThe config lives at
Minimal config
Full example
Top-level fields
Button fields
Command step fieldsTwo step types, discriminated by
CommandsAvailable from the command palette (
Visual editorRun
Keyboard shortcutsEvery command and every deck button can be bound to a key. Binding extension commandsOpen File → Preferences → Keyboard Shortcuts ( Binding individual buttonsThe
The match precedence is
For ambiguous titles (same title in different categories), use the object form:
If the target isn't found, you get an error toast naming the missing identifier. The visual editor includes a Copy keybinding button per card that copies a ready-to-paste snippet to your clipboard — paste it into Pressing the bound key while the button is already running cancels it (same click-to-cancel behavior as the deck UI). Running stateWhile a button's commands are executing, the button shows a spinner with a dimmed icon, and its top border switches to the VS Code progress color and pulses. Hovering reveals a red stop square — clicking the running button kills the process tree (Windows: IconsThe
Emoji or plain text, rendered as-is.
URL to an image (http, https, or
Relative or absolute file path.
Inline SVG. Sized automatically to the configured icon size (default 28×28; override with the top-level CategoriesAny button with a non-empty
Renders as:
Click a category header to collapse/expand it. The collapsed state is remembered per-category across config edits and window reloads. Auto-detection:
|
| Category | Detected by | Buttons |
|---|---|---|
| General | always | Save All, Format, Terminal, Command Palette |
| Git | .git/ directory |
Pull, Push, Sync, Source Control |
| NPM | package.json |
npm install + one button per script in scripts |
| Yarn | yarn.lock + package.json |
yarn install |
| pnpm | pnpm-lock.yaml + package.json |
pnpm install |
| Maven | pom.xml (uses ./mvnw / mvnw.cmd when a wrapper is present) |
Clean, Compile, Test, Package, Install, Clean + Install |
| Gradle | build.gradle(.kts) / settings.gradle(.kts) (prefers gradlew) |
Build, Test, Clean, Run |
| Rust | Cargo.toml |
Build, Run, Test, Check, Format, Clippy |
| Go | go.mod |
Build, Test, Run, Tidy, Vet |
| Python | pyproject.toml / requirements.txt / setup.py |
pip install, pytest, Black, Ruff |
| .NET | any *.sln / *.csproj / *.fsproj / *.vbproj |
Build, Run, Test, Clean, Restore |
| Docker | Dockerfile / docker-compose.yml / compose.yml |
Compose Up / Down / Build / Logs and/or Docker Build |
| Make | Makefile |
make, make clean, make test |
Example: multi-root workspace
root-workspace.code-workspace
├── frontend/ (package.json, Vue.js)
├── coordinator/ (pom.xml, Java)
└── autogluon/ (pyproject.toml, Python)
After running the generate command, you get categories:
- General (uncategorized Save All / Format / Terminal / Command Palette)
- Git
- frontend —
npm installplus one button per script infrontend/package.json, each withcwd: "${workspaceFolder:frontend}" - coordinator — Maven lifecycle buttons, each with
cwd: "${workspaceFolder:coordinator}" - autogluon — pytest / Black / Ruff / pip install, each with
cwd: "${workspaceFolder:autogluon}"
Safety
- The command never overwrites without asking. If
deck.jsonalready has buttons, you get a modal confirmation before they're replaced. modeandcolumnsfrom your existing config are preserved.- Your workspace files are never modified — only
.vscode/deck.json.
Shell command details
Where output goes
Each button press spawns a named terminal like Deck: Maven Clean + Install. Output streams live, exactly as if you'd typed the command. Interactive commands (anything that reads stdin) work.
Ctrl+C
Focus the terminal and press Ctrl+C — sends SIGINT to the running process and its entire process tree (so npm run build doesn't leave an orphan node compiler behind). On Windows this uses taskkill /T /F; on Unix it signals the process group.
When cancelled, the exit code is non-zero and the chain aborts (unless continueOnError: true).
Chain abort behavior
If a shell step exits non-zero, later steps in the same button don't run. The terminal shows:
[exit 1]
! exited with code 1; aborting chain
Except:
- When the failing step is the last step, the "aborting chain" line is suppressed (there's nothing to abort).
- When the step has
continueOnError: true, a non-zero exit is accepted and the next step runs anyway.
Working directory
cwd supports two VSCode-style placeholders:
${workspaceFolder}— the first workspace folder.${workspaceFolder:<name>}— a specific folder by name in a multi-root workspace.
You can also combine them with subdirectory paths: "cwd": "${workspaceFolder:frontend}/packages/app".
Shell quoting
Shell commands run via child_process.spawn with shell: true, which on Windows invokes cmd.exe /d /s /c "<command>" — the /s flag preserves inner quotes correctly for command lines with multiple quoted paths, e.g.:
{ "type": "shell", "command": "\"C:\\tools\\mvn\\bin\\mvn\" install -f \"C:\\Projects\\app\\pom.xml\"" }
Input prompts
Any string field (shell command, shell cwd, or any string inside vscode args) can contain ${input:<name>} placeholders. When you click the button, Deck prompts for each unique input upfront, then runs the chain with the values substituted.
Syntax
| Form | Behavior |
|---|---|
${input:branch} |
Prompts for branch, no default value. |
${input:branch:main} |
Prompts for branch, pre-filled with main (selected so you can type to overwrite). |
${input:port:3000} (used twice) |
Same name appears multiple times → prompts once, value reused everywhere. |
If you press Escape on any prompt, the entire chain aborts cleanly — no commands run, no status dot appears.
Examples
Prompt for a branch when checking out:
{
"title": "Checkout branch",
"icon": "🌿",
"commands": [
{ "type": "shell", "command": "git checkout ${input:branch:main}" }
]
}
Prompt for a port and use it both in the dev server and the browser-open command:
{
"title": "Dev (custom port)",
"icon": "▶️",
"commands": [
{ "type": "shell", "command": "npm run dev -- --port ${input:port:3000}" },
{ "type": "vscode", "command": "vscode.open", "args": ["http://localhost:${input:port:3000}"] }
]
}
Prompt for a commit message before committing:
{
"title": "Commit",
"icon": "💬",
"commands": [
{ "type": "shell", "command": "git commit -m \"${input:message}\"" }
]
}
Multi-input chain (release tag flow):
{
"title": "Tag release",
"icon": "🏷️",
"commands": [
{ "type": "shell", "command": "git tag -a ${input:version} -m \"${input:notes}\"" },
{ "type": "shell", "command": "git push origin ${input:version}" }
]
}
Example recipes
Save and format in one click
{
"title": "Save & Format",
"icon": "✨",
"commands": [
{ "type": "vscode", "command": "editor.action.formatDocument" },
{ "type": "vscode", "command": "workbench.action.files.saveAll" }
]
}
Maven clean-install for a monorepo subfolder
{
"title": "Coordinator: CI",
"icon": "📦",
"category": "Coordinator",
"commands": [
{ "type": "shell", "command": "mvn clean", "cwd": "${workspaceFolder:coordinator}" },
{ "type": "shell", "command": "mvn install", "cwd": "${workspaceFolder:coordinator}" }
]
}
Lint that only warns, never fails
{
"title": "Lint",
"icon": "📎",
"commands": [
{
"type": "shell",
"command": "npm run lint",
"continueOnError": true
}
]
}
Dev server in the frontend subfolder
{
"title": "Frontend Dev",
"icon": "▶️",
"category": "frontend",
"commands": [
{
"type": "shell",
"command": "npm run dev",
"cwd": "${workspaceFolder}/frontend"
}
]
}
Open a specific tool's sidebar
{
"title": "Source Control",
"icon": "🌿",
"commands": [
{ "type": "vscode", "command": "workbench.view.scm" }
]
}
Build + test + deploy chain
{
"title": "Ship It",
"icon": "🚀",
"color": "#10b981",
"commands": [
{ "type": "shell", "command": "npm run build" },
{ "type": "shell", "command": "npm test" },
{ "type": "shell", "command": "npm run deploy" },
{ "type": "vscode", "command": "workbench.action.terminal.focus" }
]
}
If build or tests fail, deploy doesn't run — the chain aborts with the failing step's exit code.
Settings
| Setting | Default | Description |
|---|---|---|
vscodeDeck.configPath |
.vscode/deck.json |
Path to the configuration file, relative to the first workspace folder. |
Finding VSCode command IDs
To find the ID of a VSCode command (for "type": "vscode" steps):
- Keyboard Shortcuts:
Ctrl+K Ctrl+S, search for the action, right-click → Copy Command ID. - Commands palette source: some extensions list their commands in
package.json; browsing an extension's source on GitHub shows them undercontributes.commands. - Running extensions:
Ctrl+Shift+P→ Developer: Show Running Extensions to confirm an extension is loaded and inspect its namespace.
Common built-in IDs:
workbench.action.files.saveAllworkbench.action.files.saveeditor.action.formatDocumentworkbench.action.terminal.newworkbench.action.terminal.toggleTerminalworkbench.action.showCommandsworkbench.view.explorer/workbench.view.scm/workbench.view.debugworkbench.action.reloadWindowgit.pull/git.push/git.sync/git.commit
Limitations
- Buttons are a flat list within each category — no nested sub-categories.
- Auto-detection scans the workspace root and one level of subdirectories. Nested projects (e.g.
frontend/packages/app) require manual configuration. - The config lives in the first workspace folder's
.vscode/even in multi-root workspaces (matching the VSCode convention fortasks.jsonandlaunch.json). - Shell step quoting follows Node's
child_process.spawn({ shell: true })rules. On Windows this iscmd.exe /d /s /c. If you need a different shell, wrap explicitly, e.g."command": "pwsh -c \"Get-Process\"".
License
MIT.