
Vscode Extension Guide
- 170 installs
- 23 repo stars
- Updated August 4, 2026
- aktsmm/agent-skills
Scaffold, structure, and implement VS Code extensions with manifests, commands, webviews, and packaging steps aligned to marketplace requirements.
About
vscode-extension-guide walks developers through creating VS Code extensions end to end: project layout, package.json contributions, command registration, webviews, testing, and publish workflow. It targets teams shipping editor-side tools, language helpers, or agent integrations as first-class marketplace extensions.
- extension manifest setup
- command and webview patterns
- TypeScript extension host
- marketplace packaging
- activation and contribution points
Vscode Extension Guide by the numbers
- 170 all-time installs (skills.sh)
- Ranked #922 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aktsmm/agent-skills --skill vscode-extension-guideAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 170 |
|---|---|
| repo stars | ★ 23 |
| Last updated | August 4, 2026 |
| Repository | aktsmm/agent-skills ↗ |
What it does
Scaffold, structure, and implement VS Code extensions with manifests, commands, webviews, and packaging steps aligned to marketplace requirements.
Files
VS Code Extension Guide
Create, develop, and publish VS Code extensions.
When to Use
- VS Code extension, extension development, vscode plugin
- Creating a new VS Code extension from scratch
- Adding commands, keybindings, or settings to an extension
- Publishing to VS Code Marketplace
Quick Start
# Scaffold new extension (recommended)
npm install -g yo generator-code
yo code
# Or minimal manual setup
mkdir my-extension && cd my-extension
npm init -y && npm install -D typescript @types/vscodeProject Structure
my-extension/
├── package.json # Extension manifest (CRITICAL)
├── src/extension.ts # Entry point
├── out/ # Compiled JS (gitignore)
├── artifacts/vsix/ # Keep local VSIX archives out of the repo root
├── images/icon.png # 128x128 PNG for Marketplace
└── .vscodeignore # Exclude files from VSIXBuilding & Packaging
npm run compile # Build once
npm run watch # Watch mode (F5 to launch debug)
mkdir -p artifacts/vsix
npx @vscode/vsce package --out artifacts/vsix/my-extension-1.0.0.vsixKeep local .vsix archives under artifacts/vsix/ instead of the repository root, and prune old local builds on a schedule so release artifacts do not pile up.
Done Criteria
- [ ] Extension activates without errors
- [ ] All commands registered and working
- [ ] Package size < 5MB (use
.vscodeignore) - [ ] README.md includes Marketplace/GitHub links
- [ ] Local VSIX artifacts stored outside the repo root and pruned regularly
Quick Troubleshooting
| Symptom | Fix |
|---|---|
| Extension not loading | Add activationEvents to package.json |
| Command not found | Match command ID in package.json/code |
| Shortcut not working | Remove when clause, check conflicts |
Reference Map
| Topic | Reference |
|---|---|
| AI Customization | references/ai-customization.md |
| Code Review Prompts | references/code-review-prompts.md |
| Code Samples | references/ai-customization.md and references/webview.md |
| TreeView | references/treeview.md |
| Webview | references/webview.md |
| Testing | references/testing.md |
| Publishing | references/publishing.md |
| Troubleshooting | references/troubleshooting.md |
Best Practices
Extension Host 境界
- Extension Host 上で動く scanner / provider / TreeView は、同じことができるなら Node 固有の
path/Buffer/ 生fsより VS Code API を優先する。Problems と実ビルドの環境差を避けやすい。 - 自分の拡張に同梱したリソースは、ユーザーのホーム配下や VS Code のインストール先を推測せず、
context.extensionUriとvscode.Uri.joinPathなど extension context から解決する。 - 他の installed extension に同梱されたリソースを読む必要がある場合も、
resources/agents|skills|prompts|instructions|hooks|mcpの既知 root と、manifest のchatAgents/chatPromptFiles宣言を優先して見る。built-in resource とは別の read-only resource として扱い、削除や再インストール導線を混ぜない。 - Runtime の診断ログは
console.logに散らさず、Output Channel ベースの logger に集約する。ユーザーがログを開ける導線も command / notification / README のどこかに用意する。
Manifest / Docs / Localization
package.jsonの commands、views、configuration、menus を変えたら、コード上の command ID / setting key と同時に確認する。- Marketplace 表示や設定説明をローカライズしている拡張では、
package.nls.jsonと対象言語のpackage.nls.*.jsonを同じ変更で更新する。 - 設定の並び順や説明を変えたら README の設定表、manifest consistency test、release notes の必要有無までまとめて見る。
Generated Sections
START/ENDmarker で囲む generated section は単一の SSOT として扱う。- 重複した marker pair を見つけたら、両方を残して追記せず、内容を統合して marker pair を1つに戻す。
命名の一貫性
公開前にパッケージ名・設定キー・コマンド名を統一:
| 項目 | 例 |
|---|---|
| パッケージ名 | copilot-scheduler |
| 設定キー | copilotScheduler.enabled |
| コマンドID | copilotScheduler.createTask |
| ビューID | copilotSchedulerTasks |
通知の一元管理
type NotificationMode = "sound" | "silentToast" | "silentStatus";
function normalizeNotificationMode(mode: unknown): NotificationMode {
switch (mode) {
case "sound":
case "silentToast":
case "silentStatus":
return mode;
default:
return "sound";
}
}
function getNotificationMode(): NotificationMode {
const config = vscode.workspace.getConfiguration("myExtension");
if (config.get<boolean>("showNotifications", true) === false) {
return "silentStatus";
}
return normalizeNotificationMode(
config.get<NotificationMode>("notificationMode", "sound"),
);
}
function notifyInfo(message: string, timeoutMs = 4000): void {
const mode = getNotificationMode();
switch (mode) {
case "silentStatus":
vscode.window.setStatusBarMessage(message, timeoutMs);
break;
case "silentToast":
void vscode.window.withProgress(
{ location: vscode.ProgressLocation.Notification, title: message },
async () => {},
);
break;
default:
void vscode.window.showInformationMessage(message);
}
}
function notifyError(message: string, timeoutMs = 6000): void {
const mode = getNotificationMode();
if (mode === "silentStatus") {
vscode.window.setStatusBarMessage(`⚠ ${message}`, timeoutMs);
console.error(message);
return;
}
void vscode.window.showErrorMessage(message);
}設定値は型注釈だけで信用せず、runtime で既知 enum へ正規化してください。設定ファイルの手編集や migration ずれで無効値が入っても通知経路を壊さないようにします。
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
## English
Copyright (c) 2025-2026 yamapan (aktsmm)
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0
International License.
You are free to:
- **Share** — copy and redistribute the material in any medium or format
- **Adapt** — remix, transform, and build upon the material
Under the following terms:
- **Attribution** — You must give appropriate credit, provide a link to the
license, and indicate if changes were made. You may do so in any reasonable manner,
but not in any way that suggests the licensor endorses you or your use.
- **NonCommercial** — You may not use the material for commercial purposes.
*(Please contact the author if you wish to use this material for commercial purposes.)*
- **ShareAlike** — If you remix, transform, or build upon the material, you must
distribute your contributions under the same license as the original.
No additional restrictions — You may not apply legal terms or technological
measures that legally restrict others from doing anything the license permits.
**AI/ML Training Restriction** — Use of this content for AI/ML training, data
mining, or other analytical purposes is prohibited without explicit permission.
Full license text: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
---
## 日本語
Copyright (c) 2025-2026 yamapan (aktsmm)
この作品はクリエイティブ・コモンズ 表示-非営利-継承 4.0 国際ライセンスの下に提供されています。
あなたは以下の条件に従う限り、自由に:
- **共有** — どのようなメディアやフォーマットでも資料を複製・再配布できます
- **翻案** — 資料をリミックス、変形、および加工することができます
以下の条件に従ってください:
- **表示** — あなたは適切なクレジットを表示し、ライセンスへのリンクを提供し、
変更があったらその旨を示さなければなりません。これらは合理的であればどのような方法で
行っても構いませんが、許諾者があなたやあなたの利用行為を支持していると示唆するような
方法は除きます。
- **非営利** — あなたは営利目的でこの資料を利用してはなりません。
(※商用利用をご希望の場合は、別途ご連絡ください。)
- **継承** — もしあなたがこの資料をリミックス、変形、または加工した場合、
あなたはあなたの貢献部分を元の作品と同じライセンスの下で配布しなければなりません。
追加的な制約は課せません — あなたは、このライセンスが他の者に許諾することを法的に
制限するような法的条項や技術的手段を適用してはなりません。
**AI/MLトレーニング制限** — 本コンテンツをAI/MLモデルのトレーニング、データマイニング、
その他の解析目的での使用は明示的な許可なく禁止されています。
ライセンス全文: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ja
---
## Special Permission for Microsoft Employees / Microsoft 社員向け特別許諾
### English
Microsoft Corporation employees are granted permission to use, copy, modify, and
distribute this material for any purpose within the scope of their employment
duties at Microsoft, including internal business use and customer-facing
activities, without the NonCommercial restriction of this license.
This special permission applies only to work performed as part of official
Microsoft business activities.
### 日本語
Microsoft Corporation の社員は、Microsoft での業務の範疇において、本資料を社内業務
および顧客対応を含むあらゆる目的で使用、複製、改変、配布することが許諾されます。
この場合、本ライセンスの「非営利」制限は適用されません。
この特別許諾は、Microsoft の公式な業務活動の一環として行われる作業にのみ適用されます。
---
## Disclaimer / 免責事項
### English
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### 日本語
本ソフトウェアは「現状のまま」で提供され、明示または黙示を問わず、商品性、
特定目的への適合性、および権利非侵害についての保証を含むがこれに限定されない、
いかなる種類の保証も伴いません。作者または著作権者は、契約行為、不法行為、
またはそれ以外であろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの
使用またはその他の扱いによって生じる一切の請求、損害、その他の責任について
責任を負いません。
VS Code AI Customization Guide
VS Code での AI カスタマイズ方法の包括的ガイド。
ファイル種別と用途
| ファイル種別 | パス/命名規則 | 用途 | 適用範囲 |
|---|---|---|---|
| copilot-instructions.md | .github/copilot-instructions.md | プロジェクト全体のコーディング規約 | 全チャットリクエストに自動適用 |
| Instructions Files | *.instructions.md | 言語/フレームワーク別ルール | applyTo パターンで条件適用 |
| Prompt Files | *.prompt.md | 再利用可能なタスク定義 | 手動で実行 |
| Custom Agents | *.agent.md | 専門エージェント | エージェント選択時に適用 |
| AGENTS.md | ルート or サブフォルダ | マルチエージェント環境用 | 全チャットに自動適用 |
| SKILLS.md | ~/.claude/skills/*/ or .claude/skills/ | 複数ツール横断スキル | エージェント判断で適用 |
Instructions File フォーマット
ヘッダー(YAML frontmatter)
---
name: Code Review # UI表示名
description: Expert code review guidelines
applyTo: "**/*.{ts,tsx,js,jsx}" # 自動適用パターン
---主要プロパティ
| プロパティ | 説明 |
|---|---|
name | UI表示名(未指定時はファイル名) |
description | 説明文 |
applyTo | 自動適用 glob パターン(未指定時は手動添付のみ) |
VS Code 設定
{
"github.copilot.chat.codeGeneration.useInstructionFiles": true,
"github.copilot.chat.reviewSelection.instructions": [
{ "text": "Review for bugs, security, and performance." },
{ "file": ".github/instructions/code-review.instructions.md" }
],
"github.copilot.chat.commitMessageGeneration.instructions": [
{ "text": "Use Conventional Commits format." }
],
"chat.instructionsFilesLocations": {
".github/instructions": true
}
}設定可能なシナリオ
| シナリオ | 設定キー |
|---|---|
| コードレビュー | github.copilot.chat.reviewSelection.instructions |
| コミットメッセージ | github.copilot.chat.commitMessageGeneration.instructions |
| PR タイトル/説明 | github.copilot.chat.pullRequestDescriptionGeneration.instructions |
Custom Agent フォーマット
---
name: Code Reviewer
description: Expert code reviewer
tools:
- codebase
- terminal
- githubRepo
---
# Code Reviewer Agent
You are a senior code reviewer...
## Your Role
- Conduct thorough code reviews
- Identify bugs, security issues, and performance problemsNote: In .prompt.md, tools: is an allowlist and overrides the selected or referenced agent's tools for that prompt run. Omit tools: in general prompt files unless the prompt intentionally restricts capabilities. Put stable role/tool boundaries in .agent.md instead.
ディレクトリ構造例
.github/
├── copilot-instructions.md # プロジェクト全体
├── instructions/
│ ├── code-review.instructions.md
│ ├── typescript.instructions.md
│ └── security.instructions.md
├── prompts/
│ ├── review-pr.prompt.md
│ └── refactor-file.prompt.md
└── agents/
├── code-reviewer.agent.md
└── planner.agent.md公式リソース
| リソース | URL |
|---|---|
| Custom Instructions | https://code.visualstudio.com/docs/copilot/customization/custom-instructions |
| Prompt Files | https://code.visualstudio.com/docs/copilot/customization/prompt-files |
| Custom Agents | https://code.visualstudio.com/docs/copilot/customization/custom-agents |
| Agent Skills | https://code.visualstudio.com/docs/copilot/customization/agent-skills |
| Awesome Copilot | https://github.com/github/awesome-copilot |
Tips
- glob パターン:
applyTo: "**/*.py"で Python ファイルのみに適用 - ツール参照: 本文内で
#tool:githubRepoでツールを参照可能 - 生成コマンド:
Chat: Configure Instructionsから Instructions ファイルを自動生成可能 - 同期: Settings Sync で Instructions ファイルをデバイス間同期可能
Code Review Prompts & Templates
VS Code / AI チャットで使えるコードレビュー用プロンプト集。
6観点レビューフレームワーク
コードレビュー時に確認すべき6つの観点:
| 観点 | 確認内容 |
|---|---|
| 🐛 バグ・論理エラー | ランタイムエラー、エッジケース、null/undefined 問題 |
| 🔒 セキュリティ | XSS、インジェクション、機密データ露出 |
| ⚡ パフォーマンス | N+1 クエリ、不要な再レンダリング、メモリリーク |
| 📖 保守性・可読性 | 命名、コード構造、複雑度 |
| 🧪 テストカバレッジ | 不足しているテストケース |
| 📚 ドキュメント | コメント、JSDoc、README 更新 |
構造化フィードバック形式
レビュー結果を以下の形式で出力:
- ❌ Critical: 必須修正(マージ前に修正必須)
- ⚠️ Warning: 推奨修正(対応すべき)
- 💡 Suggestion: 改善案(あると良い)
- ✅ Positive: 良い点(称賛)
---
プロンプトテンプレート
1. シンプルPRレビュー
Please analyze the changes in this PR and focus on identifying critical issues:
- Potential bugs or issues
- Performance
- Security
- Correctness
If critical issues are found, list them in a few short bullet points.
If no critical issues are found, provide a simple approval.
Sign off with: ✅ (approved) or ❌ (issues found).
Keep response concise. Only highlight critical issues that must be addressed.2. 包括的コードレビュー
あなたはシニアソフトウェアエンジニアです。以下のコードを厳しくレビューしてください:
## 確認項目
- 🐛 バグの可能性・エッジケース
- 🔒 セキュリティ脆弱性
- ⚡ パフォーマンス問題
- 📖 可読性・保守性
- 🧪 テストカバレッジ
- 📚 ドキュメント品質
## 出力形式
- ❌ Critical: 必須修正
- ⚠️ Warning: 推奨修正
- 💡 Suggestion: 改善案
問題があれば具体的な行番号と改善案を提示してください。3. Git Diff レビュー(Redditで人気)
Do a git diff and pretend you're a senior dev doing a code review4. セキュリティ特化レビュー
Review this code focusing exclusively on security vulnerabilities:
1. **Input Validation**: Check for missing validation
2. **Authentication/Authorization**: Verify access controls
3. **Data Exposure**: Look for sensitive data leaks
4. **Injection**: Check for SQL/NoSQL/Command injection
5. **XSS**: Verify output encoding
6. **CSRF**: Check token validation
7. **Dependencies**: Note any known vulnerable packages
Rate severity: 🔴 Critical | 🟠 High | 🟡 Medium | 🟢 Low5. TypeScript/React 特化
Review this TypeScript/React code for:
1. **Type Safety**: Any ny types that should be specific?
2. **React Patterns**: Hooks rules, key props, memo usage
3. **State Management**: Unnecessary re-renders, state location
4. **Error Boundaries**: Missing error handling
5. **Accessibility**: Missing ARIA attributes, keyboard navigation
Provide specific fixes with code examples.---
Instructions File テンプレート
code-review.instructions.md
---
name: Code Review
description: Expert code review guidelines
applyTo: "**/*.{ts,tsx,js,jsx,py}"
---
# Code Review Instructions
You are a senior software engineer conducting a thorough code review.
## Review Checklist
1. **Bugs & Logic Errors**: Runtime errors, edge cases, null handling
2. **Security**: XSS, injection, sensitive data exposure
3. **Performance**: N+1 queries, unnecessary re-renders, memory leaks
4. **Maintainability**: Readability, naming, code structure
5. **Type Safety**: Proper TypeScript types
6. **Test Coverage**: Missing test cases
## Output Format
- ❌ **Critical**: Must fix before merge
- ⚠️ **Warning**: Should address
- 💡 **Suggestion**: Nice to have
Keep feedback constructive and specific with line references.---
Custom Agent テンプレート
code-reviewer.agent.md
---
name: Code Reviewer
description: Expert code reviewer for thorough PR analysis
tools:
- codebase
- terminal
- githubRepo
---
# Code Reviewer Agent
You are a senior code reviewer with expertise in:
- TypeScript/JavaScript
- React/Next.js
- Node.js backend
- Testing best practices
## Your Role
- Conduct thorough code reviews
- Identify bugs, security issues, and performance problems
- Suggest improvements with concrete examples
- Be constructive and educational
## Review Process
1. Understand the context and purpose of changes
2. Check for bugs and edge cases
3. Evaluate code quality and maintainability
4. Verify test coverage
5. Provide actionable feedback
## Response Format
- **Summary**: Brief overview of changes
- **Critical Issues**: Must-fix items
- **Suggestions**: Improvements to consider
- **Positive Notes**: What was done well---
外部リソース
| リソース | 説明 | URL |
|---|---|---|
| Awesome Reviewers | 3000+ レビュープロンプト | https://github.com/baz-scm/awesome-reviewers |
| Awesome Claude Code | スキル・フック・コマンド集 | https://github.com/hesreallyhim/awesome-claude-code |
| Claude Code System Prompts | 公式プロンプト抽出 | https://github.com/Piebald-AI/claude-code-system-prompts |
| Awesome Copilot | 公式コミュニティ例 | https://github.com/github/awesome-copilot |
Publishing to Marketplace
Complete guide for publishing your VS Code extension.
Prerequisites
1. Publisher account at marketplace.visualstudio.com/manage 2. Personal Access Token (PAT) from Azure DevOps 3. vsce CLI installed: npm install -g @vscode/vsce
Creating a Publisher
1. Go to marketplace.visualstudio.com/manage 2. Sign in with Microsoft account 3. Click "Create publisher" 4. Fill in:
- ID: Unique identifier (used in extension ID)
- Name: Display name
- Description: Optional
Getting Personal Access Token (PAT)
1. Go to dev.azure.com 2. Sign in → User Settings (top right) → Personal access tokens 3. Click New Token 4. Configure:
- Name: "VS Code Marketplace" (or any descriptive name)
- Organization: All accessible organizations ← Critical!
- Expiration: Pick a real future date such as
1 year. TheCustom definedfield defaults to today's date in some Azure DevOps UIs, so a token issued without changing it is valid only for the current day —vsce verify-patpasses the same day, butvsce publishfails withAccess Denied: The Personal Access Token used has expired.the moment the day rolls over. - Scopes: Click Show all scopes if
Marketplaceis hidden, then under Marketplace check Manage (preferred —Publishalone may be rejected by some publish API paths even whenverify-patsucceeds)
5. Click Create and copy token immediately (shown only once)
Before publishing, verify the token from the same terminal session that will run vsce:
npx --yes vsce verify-pat -p "$env:VSCE_PAT"If verify-pat fails but VSCE_PAT exists in the User environment, reload it into the current process before retrying:
$env:VSCE_PAT = [System.Environment]::GetEnvironmentVariable("VSCE_PAT", "User")
npx --yes vsce verify-pat -p "$env:VSCE_PAT"If you control the repository workflow, prefer a wrapper script over repeating manual environment-variable recovery steps. A small PowerShell wrapper can validate the current Process VSCE_PAT first, automatically fall back to the User-scoped VSCE_PAT when VS Code is still holding an expired process value, and then forward vsce verify-pat, vsce show, or vsce publish with the resolved token. This avoids the common failure mode where the User environment is correct but VS Code child processes still inherit a stale token from the older process environment.
Login and Publish
# Login (first time or when token expires)
npx @vscode/vsce login <publisher-id>
# Paste PAT when prompted
# Verify login
npx @vscode/vsce ls-publishers
# Verify the PAT used by this terminal before publish
npx --yes vsce verify-pat -p "$env:VSCE_PAT"
# Publish new version
npx @vscode/vsce publish
# Publish an already-built VSIX (prevents packaging the wrong artifact)
npx @vscode/vsce publish -i ./my-extension-1.0.0.vsix
# Confirm an already-published version without failing the release script
npx @vscode/vsce publish -i ./my-extension-1.0.0.vsix --skip-duplicate
# Publish with version bump
npx @vscode/vsce publish minor # 0.1.0 → 0.2.0
npx @vscode/vsce publish patch # 0.1.0 → 0.1.1vsceoption names vary by version. If--packagePathis rejected, check the localvsce publish --helpand prefer the supported package input option such as-i. Do not paste help output into public logs if it displays PAT defaults.
Pre-publish Checklist
| Item | Check |
|---|---|
publisher in package.json | Matches your publisher ID |
version | Incremented from previous |
README.md | Exists (lowercase!) and has content |
LICENSE | Included |
icon | 128x128 PNG, path in package.json |
.vscodeignore | Excludes unnecessary files |
package.json Requirements
{
"name": "my-extension",
"displayName": "My Extension",
"description": "Brief description for Marketplace",
"version": "1.0.0",
"publisher": "your-publisher-id",
"icon": "images/icon.png",
"repository": {
"type": "git",
"url": "https://github.com/user/repo"
},
"categories": ["Other"],
"keywords": ["keyword1", "keyword2"]
}Valid Categories
Programming Languages, Snippets, Linters, Themes, Debuggers,
Formatters, Keymaps, SCM Providers, Other, Extension Packs,
Language Packs, Data Science, Machine Learning, Visualization,
Notebooks, Education, Testing, AI, ChatVersion Constraints
- ✅ Valid:
1.0.0,1.2.3,0.0.1 - ❌ Invalid:
1.0.0-beta.1,1.0.0-rc1(prerelease tags rejected) - Use GitHub Releases for beta distribution instead
Inspect Package Before Publishing
# List files that will be included
npx @vscode/vsce ls
# Create VSIX without publishing (for inspection)
mkdir -p artifacts/vsix
npx @vscode/vsce package --out artifacts/vsix/my-extension-1.0.0.vsixIf the project has a repository-specific release hygiene test, treat that test as the source of truth for payload safety. vsce ls flags differ between CLI versions, while a project test can assert the exact entrypoint and excluded files required by that extension.
Local VSIX Artifact Hygiene
Store generated .vsix files under artifacts/vsix/ rather than the repository root. This keeps the root readable, makes cleanup scriptable, and reduces the chance of attaching or inspecting the wrong local file.
New-Item -ItemType Directory -Force artifacts/vsix | Out-Null
npx @vscode/vsce package --out artifacts/vsix/my-extension-1.0.0.vsix
npx @vscode/vsce publish -i ./artifacts/vsix/my-extension-1.0.0.vsixWhen you keep historical local builds, set a retention rule and prune old archives automatically. Keeping only the latest 10 local VSIX files is usually enough for rollback and spot-checking.
$vsixDir = "artifacts/vsix"
Get-ChildItem $vsixDir -Filter "my-extension-*.vsix" |
Sort-Object { [version]($_.BaseName -replace '^my-extension-', '') } -Descending |
Select-Object -Skip 10 |
Remove-Item -ForceIf the project ships multiple package variants such as a release VSIX and a dev/coexistence VSIX, keep all of them under artifacts/vsix/ except the one release artifact you intentionally attach. Apply the same hygiene checks to every variant so the smaller test build does not silently diverge from the release payload.
.vscodeignore
Minimize package size:
**
!package.json
!README.md
!LICENSE
!CHANGELOG.md
!out/**
!images/icon.png
src/**
test/**
node_modules/**
*.ts
tsconfig*.json
.github/**
.vscode/**
*.vsix
artifacts/**Updating Published Extensions
# Increment version and publish
npx @vscode/vsce publish patch
# Or manually update version first
npm version patch
npx @vscode/vsce publishUnpublishing
# Unpublish specific version
npx @vscode/vsce unpublish <publisher>.<extension> --version <version>
# Unpublish entire extension (use with caution!)
npx @vscode/vsce unpublish <publisher>.<extension>Common Errors
| Error | Cause | Fix |
|---|---|---|
Missing publisher | No publisher in package.json | Add "publisher": "your-id" |
Personal Access Token... | PAT invalid or expired | Regenerate PAT with correct scopes |
Access Denied... PAT used has expired | The current VSCE_PAT value is expired, the open terminal still has an old value, the PAT was issued with Custom defined expiration defaulting to today, or the PAT lacks Marketplace > Manage scope (so verify-pat passes but publish is rejected) | Regenerate the PAT with a real future expiration and Marketplace > Manage scope, update VSCE_PAT, reload the current process, and run vsce verify-pat before publish |
version already exists | Same version published | Increment version number |
README not found | File missing or wrong case | Create README.md (lowercase) |
invalid prerelease | Version like 1.0.0-beta | Use standard version format |
unknown option | Local vsce version differs | Check vsce <command> --help and use supported flags |
Release Completion Contract
When the user explicitly asks to release a VS Code extension, do not stop at a version bump, commit, or push. Treat the release as incomplete until all of these are done or explicitly blocked:
1. Package the VSIX under artifacts/vsix/. 2. Inspect the VSIX contents or run the repo-specific package integrity test. 3. Install the generated VSIX locally with code --install-extension ... --force. 4. Publish the exact VSIX to Marketplace. 5. Create and push the release tag. 6. Create the GitHub Release with the VSIX attached. 7. Verify through at least two non-stale channels, such as publish success output, gh release view, and git ls-remote --tags.
If a blocker appears after the version bump, report the state separately: Version, VSIX, Marketplace publish, Git tag, and GitHub Release.
GitHub Release After Marketplace Publish
When attaching the VSIX to a GitHub Release, pin the release to a full commit SHA if you use --target. Short SHAs can be rejected by the GitHub API.
$full = git rev-parse HEAD
gh release create v1.0.0 .\artifacts\vsix\my-extension-1.0.0.vsix --target $full --title "v1.0.0 - Release title" --notes-file .\release-notes-v1.0.0.mdIf you already calculate the VSIX checksum locally, record the size and SHA256 digest in the release notes too. GitHub Release asset metadata then becomes an independent proof of exactly which artifact was published, which is useful when Marketplace metadata is still stale right after publish.
$vsix = ".\artifacts\vsix\my-extension-1.0.0.vsix"
Get-Item $vsix | Select-Object Name, Length
Get-FileHash $vsix -Algorithm SHA256 | Select-Object HashAfter publishing, vsce show output can lag or sort versions unexpectedly. If you need a deterministic confirmation, run duplicate-safe publish against the exact VSIX and verify that the Marketplace reports the version as already published.
Marketplace metadata can be stale immediately after a successful publish. If vsce show --json or the public Marketplace page still shows the previous version, do not republish or bump the version just from that signal. First verify the GitHub Release and remote tag:
gh release view vX.Y.Z --json "tagName,name,url,isDraft,isPrerelease,publishedAt"
git ls-remote --tags origin vX.Y.ZIf vsce publish reported success and GitHub Release plus remote tag are present, treat the Marketplace mismatch as propagation delay and recheck later.
Marketplace URLs
- Your extensions:
https://marketplace.visualstudio.com/manage/publishers/<publisher-id> - Published extension:
https://marketplace.visualstudio.com/items?itemName=<publisher>.<extension> - Statistics: Available in manage portal after publish
PAT Security & Persistence
Persist VSCE_PAT safely (Windows)
# 1. Set for the current terminal session (type directly – never paste into chat!)
$env:VSCE_PAT = "<your-pat>"
# 2. Persist to User environment variables (survives reboots)
[Environment]::SetEnvironmentVariable("VSCE_PAT", $env:VSCE_PAT, "User")
# 3. Verify without revealing the value
if ($env:VSCE_PAT) { "present (length: $($env:VSCE_PAT.Length))" } else { "missing" }⚠️ SetEnvironmentVariable does not update already-open terminals.Open a new terminal (or restart VS Code) after persisting.
If a publish command still uses an expired token after you update the User environment, the current terminal probably kept the old process value. Reassign $env:VSCE_PAT from the User value in that terminal, then run verify-pat again.
If the PAT was accidentally exposed
1. Revoke immediately at dev.azure.com → User Settings → Personal access tokens → Revoke 2. Generate a new token (same scopes) 3. Update VSCE_PAT with the new value
Rules
- ❌ Never paste a PAT into chat, issue comments, or commit messages
- ❌ Never echo
$env:VSCE_PAT– check existence/length only - ❌ Avoid sharing raw
vsce publish --helpoutput whenVSCE_PATis set; some versions display the effective PAT default in help text - ✅ Use
VSCE_PATenv var;vsce publishpicks it up automatically - ✅ Set expiry ≤ 1 year and rotate on a schedule
.vscodeignore – Recommended Exclusion Patterns
Keep the published VSIX small and free of dev-only artefacts:
# Source & config (already compiled to out/)
src/**
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts
!out/**
# Dev tooling
.vscode/**
.vscode-test/**
.github/**
node_modules/**
# Dev-only content (never ship to users)
docs/**
output/**
output_sessions/**
research/**
session/**
FULL_SPECIFICATION.md
AGENTS.md
# Secondary docs or local artifacts that are not needed in the VSIX
README_ja.md
artifacts/**
# Large or unnecessary assets
images/demo-animated.gif
*.vsixTip: Run npx @vscode/vsce ls to preview exactly what will be packagedbefore runningvsce packageorvsce publish.
Judging node_modules/** exclusion
Before excluding node_modules/**, confirm out/*.js only requires vscode and Node built-ins, with no live external imports:
Select-String -Path out\*.js -Pattern 'require\("([^.][^"]+)"\)' -AllMatches
Select-String -Path out\*.js -Pattern 'import\("[^.]' # dynamic importsA dependencies entry that is only reached through a guarded dynamic import(...) disabled in the extension host (e.g. a CLI-side SDK that exits early when vscode is present) ships its entire transitive tree as dead weight. One real case: @github/copilot-sdk → @github/copilot ≈ 285 MB → packaged VSIX 181 MB. After moving the unused dep out and excluding node_modules/**, the same VSIX dropped to ~45 KB (≈4000× smaller). Compare VSIX size against the previous release; an unchanged-huge size usually means .vscodeignore is not actually excluding node_modules/**.
Marketplace auto-resolves relative-path images
When the README references images by relative path (e.g. ), the Marketplace web view and the in-VS Code extension details pane both resolve those paths against repository.url in package.json and fetch the file from raw.githubusercontent.com/<owner>/<repo>/<branch>/<path>. So as long as the image is committed and pushed to the default branch, you can keep it out of the VSIX to drop multi-megabyte demo media without breaking the listing.
This auto-resolution applies to images, not to arbitrary Markdown links. If you exclude secondary documents such as README_ja.md from the VSIX, link to them with an absolute GitHub URL from the primary README.md instead of a relative Markdown link.
A single 15 MB demo GIF can shrink a VSIX from ~15 MB to ~175 KB (≈99% reduction) with no visible difference in Marketplace rendering.
Verify VSIX integrity before publish
vsce ls validates .vscodeignore filtering, but it cannot detect a truncated or zip-corrupt VSIX (which can happen when the package step is interrupted by build watchers or transient I/O). Always do a local install round-trip before vsce publish:
$cli = "$env:LOCALAPPDATA\Programs\Microsoft VS Code\bin\code.cmd"
& $cli --install-extension artifacts\vsix\my-extension-1.0.0.vsix --force
# If you see:
# Error: End of central directory record signature not found.
# the VSIX is truncated; rebuild it with `vsce package` and re-test.Also treat vsce package completion based on the output file (size + mtime), not on console messages — terminal capture sometimes drops the DONE Packaged: ... line, but the artifact on disk is the source of truth. If the VSIX exists but ZIP inspection fails, check whether node / vsce is still writing the file. Once no package process remains, delete the corrupt artifact, rebuild with a deterministic output path, and inspect that rebuilt file instead of reusing the partial archive.
Get-ChildItem artifacts/vsix/my-extension-1.0.0.vsix |
Select-Object Length, LastWriteTimeIf the extension manifest references icons such as icon.png for the Marketplace tile and icon.svg for activity bar or command UI, add a release check that asserts the referenced files physically exist before packaging.
Post-publish Verification
vsce show --json is useful, but its metadata can lag right after publish. Treat the publish command's result as the first source of truth and use at least one more independent check.
- Run duplicate-safe publish against the exact VSIX and confirm
already published - If you use Git tags or GitHub Releases, verify the release/tag exists too
- If the Marketplace listing lags, do not republish a new version just because
vsce show still returns the previous metadata snapshot
- If publish is paused by review, auth, duplicate, or permissions, report version, artifact, checksum, commit, tag, push, and publish state separately so the same VSIX can be resumed without guessing.
Testing VS Code Extensions
Set up and run tests using @vscode/test-electron.
Setup
npm install -D @vscode/test-electron mocha @types/mocha globProject Structure
my-extension/
├── src/
│ └── extension.ts
├── test/
│ ├── runTest.ts # Test runner entry
│ └── suite/
│ ├── index.ts # Mocha configuration
│ └── extension.test.ts # Test file
├── tsconfig.json
└── tsconfig.test.jsontsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "out"
},
"include": ["src/**/*", "test/**/*"]
}test/runTest.ts
import * as path from "path";
import { runTests } from "@vscode/test-electron";
async function main() {
try {
const extensionDevelopmentPath = path.resolve(__dirname, "../../");
const extensionTestsPath = path.resolve(__dirname, "./suite/index");
await runTests({
extensionDevelopmentPath,
extensionTestsPath,
// Optional: specify VS Code version
// version: '1.85.0',
// Optional: open specific workspace
// launchArgs: ['--disable-extensions', path.resolve(__dirname, '../../test-workspace')],
});
} catch (err) {
console.error("Failed to run tests");
process.exit(1);
}
}
main();test/suite/index.ts
import * as path from "path";
import Mocha from "mocha";
import { glob } from "glob";
export async function run(): Promise<void> {
const mocha = new Mocha({
ui: "tdd",
color: true,
timeout: 10000,
});
const testsRoot = path.resolve(__dirname, ".");
const files = await glob("**/**.test.js", { cwd: testsRoot });
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
return new Promise((resolve, reject) => {
mocha.run((failures) => {
if (failures > 0) {
reject(new Error(`${failures} tests failed.`));
} else {
resolve();
}
});
});
}test/suite/extension.test.ts
import * as assert from "assert";
import * as vscode from "vscode";
suite("Extension Test Suite", () => {
vscode.window.showInformationMessage("Start all tests.");
test("Extension should be present", () => {
const ext = vscode.extensions.getExtension("publisher.extension-name");
assert.ok(ext, "Extension not found");
});
test("Extension should activate", async () => {
const ext = vscode.extensions.getExtension("publisher.extension-name");
await ext?.activate();
assert.ok(ext?.isActive, "Extension not activated");
});
test("Command should be registered", async () => {
const commands = await vscode.commands.getCommands();
assert.ok(commands.includes("myExt.hello"), "Command not registered");
});
test("Command should execute without error", async () => {
await assert.doesNotReject(vscode.commands.executeCommand("myExt.hello"));
});
});package.json Scripts
{
"scripts": {
"compile": "tsc -p ./",
"compile-tests": "tsc -p tsconfig.test.json",
"pretest": "npm run compile && npm run compile-tests",
"test": "node ./out/test/runTest.js"
}
}Running Tests
# Run all tests
npm test
# Tests will:
# 1. Download VS Code (if needed)
# 2. Launch VS Code with extension loaded
# 3. Execute test suite
# 4. Exit with result codeRisk-Based Regression Checks
Run a full compile first, then add targeted checks based on what changed.
| Change area | Extra checks |
|---|---|
Commands / settings / views in package.json | Verify manifest consistency, command IDs, setting keys, menu when clauses, and README setting tables |
package.nls.json or localized manifest text | Compare all localized key sets and confirm missing keys fail tests |
| Runtime logging / diagnostics | Verify logs go through an Output Channel logger instead of direct console.* calls in extension runtime paths |
| Resource scanners / providers | Test with extension-host APIs available and with missing/empty roots; avoid relying on local filesystem guesses; if you scan installed extensions, cover both known resources/* roots and manifest-declared chatAgents / chatPromptFiles paths |
| Selectors / quick actions / saved options | Hide internal, test, deprecated, stale, or unsupported candidates; preserve newly introduced normal candidates; confirm hidden saved values do not reappear from settings, cache, or fallback paths |
| Installer / updater / index merge logic | Run focused regression scripts plus a broader smoke test because these paths often cross manifest, filesystem, and network boundaries |
| Generated marker sections | Test duplicate marker handling and confirm the final file contains exactly one generated section pair |
For small fixes, a good baseline is npm run compile plus the smallest script or test file that exercises the changed behavior. For shared manifest, installer, updater, or scanner code, prefer adding one regression test over relying only on manual verification.
Common Test Patterns
Testing with Documents
test("Should modify document", async () => {
const doc = await vscode.workspace.openTextDocument({
content: "hello",
language: "plaintext",
});
const editor = await vscode.window.showTextDocument(doc);
await editor.edit((editBuilder) => {
editBuilder.insert(new vscode.Position(0, 5), " world");
});
assert.strictEqual(doc.getText(), "hello world");
});Testing Settings
test("Should read configuration", () => {
const config = vscode.workspace.getConfiguration("myExt");
const value = config.get<string>("greeting");
assert.strictEqual(value, "Hello");
});Waiting for Events
test("Should handle file save", async () => {
const doc = await vscode.workspace.openTextDocument({ content: "test" });
const savePromise = new Promise<void>((resolve) => {
const disposable = vscode.workspace.onDidSaveTextDocument((saved) => {
if (saved === doc) {
disposable.dispose();
resolve();
}
});
});
await doc.save();
await savePromise;
});CI Integration
.github/workflows/test.yml:
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: xvfb-run -a npm testNote: xvfb-run is required on Linux for headless VS Code testing.
TreeView Implementation
Create sidebar views for your VS Code extension.
package.json Configuration
"contributes": {
"viewsContainers": {
"activitybar": [{
"id": "myExtContainer",
"title": "My Extension",
"icon": "images/icon.svg"
}]
},
"views": {
"myExtContainer": [{
"id": "myExtView",
"name": "Items"
}]
}
}View locations:
| Container | Description |
|---|---|
explorer | File Explorer sidebar |
scm | Source Control sidebar |
debug | Debug sidebar |
test | Testing sidebar |
| Custom ID | Activity bar (new icon) |
TreeDataProvider Implementation
import * as vscode from "vscode";
// Tree item class
class MyItem extends vscode.TreeItem {
constructor(
public readonly label: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public readonly children?: MyItem[],
) {
super(label, collapsibleState);
this.tooltip = this.label;
this.contextValue = "myItem"; // For context menu
}
}
// Provider class
class MyTreeProvider implements vscode.TreeDataProvider<MyItem> {
// Event emitter for refresh
private _onDidChangeTreeData = new vscode.EventEmitter<MyItem | undefined>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
private data: MyItem[] = [
new MyItem("Parent", vscode.TreeItemCollapsibleState.Expanded, [
new MyItem("Child 1", vscode.TreeItemCollapsibleState.None),
new MyItem("Child 2", vscode.TreeItemCollapsibleState.None),
]),
];
refresh(): void {
this._onDidChangeTreeData.fire(undefined);
}
getTreeItem(element: MyItem): vscode.TreeItem {
return element;
}
getChildren(element?: MyItem): MyItem[] {
return element ? element.children || [] : this.data;
}
}Registration in extension.ts
export function activate(context: vscode.ExtensionContext) {
const provider = new MyTreeProvider();
// Register provider
vscode.window.registerTreeDataProvider("myExtView", provider);
// Or use createTreeView for more control
const treeView = vscode.window.createTreeView("myExtView", {
treeDataProvider: provider,
showCollapseAll: true,
});
context.subscriptions.push(treeView);
// Refresh command
context.subscriptions.push(
vscode.commands.registerCommand("myExt.refresh", () => provider.refresh()),
);
}Item Customization
class MyItem extends vscode.TreeItem {
constructor(label: string, isFolder: boolean) {
super(
label,
isFolder
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None,
);
// Icon (codicon or file path)
this.iconPath = new vscode.ThemeIcon(isFolder ? "folder" : "file");
// Click action
this.command = {
command: "myExt.openItem",
title: "Open",
arguments: [this],
};
// Description (gray text after label)
this.description = "(modified)";
}
}Context Menu
"contributes": {
"menus": {
"view/item/context": [{
"command": "myExt.delete",
"when": "view == myExtView && viewItem == myItem"
}]
}
}Troubleshooting
Common issues and solutions for VS Code extension development.
Extension Not Loading
| Symptom | Cause | Solution |
|---|---|---|
| Extension never activates | Missing activationEvents | Add to package.json: "activationEvents": ["onStartupFinished"] |
| "Extension is not active" | Wrong activation trigger | Use "*" to always activate (dev only) or specific event |
| Works in dev, not installed | Build output not included | Check .vscodeignore, ensure out/ is included |
Debug Activation
// Add at top of activate() while debugging, or route this through your logger.
const output = vscode.window.createOutputChannel("My Extension");
output.appendLine("Extension activating...");
output.show(true);Prefer Output Channel logs for extension diagnostics. Use Help → Toggle Developer Tools → Console only for temporary investigation or webview/runtime errors that are not reaching your logger.
Command Not Found
| Symptom | Cause | Solution |
|---|---|---|
| "command not found" | ID mismatch | Ensure same ID in package.json and registerCommand() |
| Command not in palette | Missing contributes.commands | Add command definition to package.json |
| Command defined but fails | Extension not activated | Check activationEvents includes the command |
Verify Command Registration
// In activate()
const output = vscode.window.createOutputChannel("My Extension");
const commands = await vscode.commands.getCommands();
output.appendLine(
`Registered: ${commands.filter((c) => c.includes("myExt")).join(", ")}`,
);Keyboard Shortcuts Not Working
| Symptom | Cause | Solution |
|---|---|---|
| Shortcut does nothing | when clause too restrictive | Remove or broaden when condition |
| Works sometimes | Context-dependent when | Check active editor, focus state |
| Conflict with other | Another extension/VS Code uses it | Use unique key combination |
Check for Conflicts
1. Ctrl+K Ctrl+S → Open Keyboard Shortcuts 2. Search for your key combination 3. Look for conflicts (multiple entries)
Common when Issues
// ❌ Doesn't work in editor
"when": "!inputFocus"
// ✅ Works everywhere
"when": "" // or omit entirely
// ✅ Only in editor with text focus
"when": "editorTextFocus"Packaging Issues
| Symptom | Cause | Solution |
|---|---|---|
| VSIX too large (100MB+) | node_modules shipped, incl. a huge transitive dep | Exclude node_modules/** in .vscodeignore when out/ needs no external runtime packages (see below) |
| Files missing in VSIX | Over-aggressive ignore | Use npx @vscode/vsce ls to check |
| Icon not showing | Wrong path or format | Use 128x128 PNG, check path in package.json |
End of central directory record signature not found on install | Truncated / corrupt VSIX (build interrupted) | Re-run vsce package; verify with code --install-extension <vsix> --force before publish |
Inspect VSIX Contents
# List what will be packaged
npx @vscode/vsce ls
# Extract and inspect VSIX
unzip -l my-extension-1.0.0.vsixWhen it is safe to exclude node_modules/** entirely
A bundled extension (esbuild/webpack) needs no node_modules in the VSIX. An unbundled extension only needs the packages its compiled out/ actually requires at runtime. Check before trusting dependencies:
# What does the compiled output actually require at runtime?
Select-String -Path out\*.js -Pattern 'require\("([^.][^"]+)"\)' -AllMatches |
ForEach-Object { $_.Matches } | ForEach-Object { $_.Groups[1].Value } |
Sort-Object -Unique
# Also scan for dynamic import("pkg")If the only externals are vscode (provided by the host) and Node built-ins (fs, path, http, child_process, ...), add node_modules/** to .vscodeignore and ship none of it. A dependency that is only reached through a guarded dynamic `import()` disabled inside the extension host is dead weight — e.g. @github/copilot-sdk pulls a ~285MB @github/copilot tree that kept one VSIX at 181MB; excluding node_modules produced an identical-functioning ~45KB build.
Always list every entry, not just the size
# Enumerate all VSIX entries and flag leaked temp files
Add-Type -AssemblyName System.IO.Compression.FileSystem
$z = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path my-extension-1.0.0.vsix))
$z.Entries | Sort-Object FullName | ForEach-Object { '{0,8} {1}' -f $_.Length, $_.FullName }
$z.Dispose()Block the build if temp runner scripts (_*.ps1), logs, or stray *.vsix leaked in, and add the matching ignore patterns (*.ps1, *.log, *.vsix) to .vscodeignore. Compare the new VSIX size against the previous version: an unexpectedly large or unchanged-huge size means .vscodeignore is not excluding node_modules.
Publishing Errors
| Symptom | Cause | Solution |
|---|---|---|
| PAT invalid | Wrong scope or expired | Regenerate with Marketplace Manage scope |
| Publisher not found | ID mismatch | Verify publisher ID matches exactly |
| Version exists | Already published | Increment version number |
| README not showing | Wrong filename case | Must be README.md not README.MD |
Runtime Errors
| Symptom | Cause | Solution |
|---|---|---|
| "Cannot find module" | Dependency not bundled | Add to dependencies (not devDependencies) or bundle |
| API undefined | Wrong VS Code version | Check engines.vscode matches API used |
| Permission denied | Restricted API | Check extension permissions/capabilities |
Check VS Code API Version
// package.json - specify minimum VS Code version
"engines": {
"vscode": "^1.80.0"
}Debug Tips
Enable Verbose Logging
const outputChannel = vscode.window.createOutputChannel("My Extension");
outputChannel.appendLine("Debug message");
outputChannel.show();Keep runtime diagnostics behind a small logger wrapper so tests can assert the logging route and production code does not accumulate stray console.log calls.
Extension Host Logs
1. Help → Toggle Developer Tools 2. Console tab 3. Filter by your extension name
Reload Without Restart
- Ctrl+Shift+P → "Developer: Reload Window"
Quick Fixes Summary
# Clean rebuild
rm -rf out/ node_modules/
npm install
npm run compile
# Reset installed extension
code --uninstall-extension publisher.extension-id
npx @vscode/vsce package
code --install-extension ./extension-1.0.0.vsix
# Check what's in your VSIX
npx @vscode/vsce lsWebview 真っ白 / SyntaxError
| 症状 | 原因 | 解決策 |
|---|---|---|
| 画面真っ白 | JavaScript SyntaxError | Webview DevTools Console でエラー確認 |
Invalid regular expression: /^*/ | 正規表現のバックスラッシュが消えた | テンプレート内で二重エスケープ (\\d, \\s) |
Unexpected token | minify時にクォートが崩れた | data-action + イベント委譲パターンに変更 |
| ボタンが反応しない | innerHTML後の onclick が効かない | document.addEventListener で委譲 |
デバッグ手順
1. Developer: Open Webview Developer Tools を実行 2. Console タブでエラーを確認 3. ビルド出力 out/extension.js で該当行を検索 4. ソースの正規表現/クォートを修正し再ビルド
命名の不一致
| 症状 | 原因 | 解決策 |
|---|---|---|
| 設定が効かない | 設定キーがコードと不一致 | package.json と getConfiguration() を統一 |
| コマンドが見つからない | コマンドIDがpackage.jsonと不一致 | 全箇所で同じIDを使用 |
命名一貫性チェック
# package.json のコマンド/設定キーを抽出
grep -E '"myExt\.' package.json
# ソースコードの使用箇所を検索
grep -r "myExt\." src/公開前に統一することを強く推奨(公開後は既存ユーザーの設定が壊れる)。
Webview Implementation
Create rich HTML-based UI panels in VS Code.
Basic Webview Panel
import * as vscode from "vscode";
export function createWebviewPanel(context: vscode.ExtensionContext) {
const panel = vscode.window.createWebviewPanel(
"myWebview", // Identifier
"My Webview", // Title
vscode.ViewColumn.One, // Editor column
{
enableScripts: true, // Enable JavaScript
retainContextWhenHidden: true, // Keep state when hidden
localResourceRoots: [
// Allowed local resources
vscode.Uri.joinPath(context.extensionUri, "media"),
],
},
);
panel.webview.html = getWebviewContent(panel.webview, context.extensionUri);
return panel;
}HTML Content
function getWebviewContent(
webview: vscode.Webview,
extensionUri: vscode.Uri,
): string {
// Get URI for local resources
const styleUri = webview.asWebviewUri(
vscode.Uri.joinPath(extensionUri, "media", "style.css"),
);
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(extensionUri, "media", "main.js"),
);
// CSP nonce for security
const nonce = getNonce();
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';">
<link href="${styleUri}" rel="stylesheet">
</head>
<body>
<h1>Hello Webview!</h1>
<button id="btn">Click Me</button>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
}
function getNonce(): string {
let text = "";
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
text += chars.charAt(Math.floor(Math.random() * chars.length));
}
return text;
}Embed initial data safely (no document.write)
<script id="initial-data" type="application/json">
${serializeForWebview(initialData)}
</script>
<script nonce="${nonce}">
(function () {
var vscode = acquireVsCodeApi();
var initialData = {};
try {
var el = document.getElementById("initial-data");
if (el && el.textContent) initialData = JSON.parse(el.textContent) || {};
} catch (e) {
initialData = {};
}
// ... use initialData ...
})();
</script>- Avoid Base64 +
document.write; inject JSON as text and parse. - Escape
<, U+2028/2029 before embedding to keep the script tag valid. - Keep the CSP nonce on the executable script only.
Message Passing
Extension → Webview
// In extension
panel.webview.postMessage({ command: "update", data: { count: 42 } });// In webview (media/main.js)
window.addEventListener("message", (event) => {
const message = event.data;
if (message.command === "update") {
console.log("Count:", message.data.count);
}
});Webview → Extension
// In webview
const vscode = acquireVsCodeApi();
document.getElementById("btn").addEventListener("click", () => {
vscode.postMessage({ command: "buttonClicked", text: "Hello!" });
});// In extension
panel.webview.onDidReceiveMessage(
(message) => {
switch (message.command) {
case "buttonClicked":
vscode.window.showInformationMessage(message.text);
return;
}
},
undefined,
context.subscriptions,
);State Persistence
// In webview - save state
const vscode = acquireVsCodeApi();
vscode.setState({ count: 5 });
// Restore state
const state = vscode.getState();
if (state) {
console.log("Restored count:", state.count);
}When an edit form shows values derived from current settings defaults, capture a normalized baseline when edit starts and diff against that frozen baseline on submit. Do not recompute the original side of the diff from current defaults, or unchanged fields can become false updates if settings change mid-edit.
let editingTaskSnapshot = null;
let editingTaskNormalizedSnapshot = null;
function normalizeTaskForDiff(task, currentDefaults) {
const source = task || {};
return {
jitterSeconds:
source.jitterSeconds != null
? Number(source.jitterSeconds)
: currentDefaults.jitterSeconds,
autoMode: source.autoMode === true,
chatSession:
source.chatSession === "new" || source.chatSession === "continue"
? source.chatSession
: "default",
};
}
function beginEdit(task, currentDefaults) {
editingTaskSnapshot = { ...task };
editingTaskNormalizedSnapshot = normalizeTaskForDiff(task, currentDefaults);
}
function buildUpdateData(formData, currentDefaults) {
const current = normalizeTaskForDiff(formData, currentDefaults);
const original =
editingTaskNormalizedSnapshot ||
normalizeTaskForDiff(editingTaskSnapshot, currentDefaults);
const diff = {};
for (const key of Object.keys(current)) {
if (current[key] !== original[key]) {
diff[key] = formData[key];
}
}
return diff;
}This matters when you correctly keep create-form defaults reactive but avoid overwriting active edit forms during updateDefaults / configuration-change events.
VS Code Theme Integration
Use CSS variables for consistent theming:
/* media/style.css */
body {
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
}
button {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: 8px 16px;
cursor: pointer;
}
button:hover {
background-color: var(--vscode-button-hoverBackground);
}Sidebar Webview (WebviewViewProvider)
For webviews in the sidebar instead of editor panels:
class MyWebviewProvider implements vscode.WebviewViewProvider {
resolveWebviewView(webviewView: vscode.WebviewView) {
webviewView.webview.options = { enableScripts: true };
webviewView.webview.html = getWebviewContent();
}
}
// Register in extension.ts
vscode.window.registerWebviewViewProvider(
"myExtSidebarView",
new MyWebviewProvider(),
);"contributes": {
"views": {
"explorer": [{
"type": "webview",
"id": "myExtSidebarView",
"name": "My Webview"
}]
}
}Fallback Patterns
Promise-based Callback Fallback
When using Promise-based callbacks (e.g., resolveCreate), always provide a fallback mechanism:
// ❌ Bad: Single callback dependency
case "createTask": {
if (!resolveCreate) {
return; // Silent failure if callback not set
}
resolveCreate(data);
break;
}
// ✅ Good: Fallback to alternative handler
case "createTask": {
const result = buildResult(data);
if (resolveCreate) {
resolveCreate(result);
resolveCreate = undefined;
} else if (onAction) {
// Fallback to action handler
onAction({ action: "create", data: result });
}
break;
}VS Code Internal API Fallback
When using internal/unstable APIs (vscode.lm, vscode.chat), always implement fallback:
// ✅ Good: API availability check + fallback
static async getAvailableModels(): Promise<Model[]> {
const models: Model[] = [{ id: "", name: "Default" }];
try {
if (typeof vscode.lm !== "undefined" && "selectChatModels" in vscode.lm) {
const available = await (vscode.lm as any).selectChatModels({});
// Null check for API result
if (available && Array.isArray(available)) {
for (const model of available) {
models.push({
id: model.id || model.family,
name: model.name || model.family || model.id,
});
}
}
}
} catch (error) {
console.log("API not available, using fallback", error);
}
// Return fallback if API returned nothing useful
if (models.length <= 1) {
return getFallbackModels();
}
return models;
}Path Consistency
When handling both local and global paths, use consistent format:
// ❌ Bad: Mixed path formats
templates.push({
source: "local",
path: relativePath, // Relative
});
templates.push({
source: "global",
path: file.fsPath, // Absolute - inconsistent!
});
// ✅ Good: Consistent relative paths
templates.push({
source: "local",
path: path.relative(workspaceRoot, file.fsPath).replace(/\\/g, "/"),
});
templates.push({
source: "global",
path: path.relative(globalRoot, file.fsPath).replace(/\\/g, "/"),
});Reliable Webview Communication Pattern
Recommended Pattern (Simple & Reliable)
Wrap the entire webview script in an IIFE and send webviewReady at the end:
` ypescript function getWebviewContent(): string { return <!DOCTYPE html>
<html> <head> <meta charset="UTF-8"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';"> </head> <body> <div id="app"></div>
<script nonce="${nonce}"> (function() { const vscode = acquireVsCodeApi();
// Initialize UI // ... DOM setup ...
// Handle messages from extension window.addEventListener('message', event => { const message = event.data; switch (message.type) { case 'updateData': renderData(message.data); break; } });
// Initial render renderUI();
// Notify extension that webview is ready (LAST!) vscode.postMessage({ type: 'webviewReady' }); })(); </script> </body> </html>; } `
Extension Side Handler
ypescript panel.webview.onDidReceiveMessage(message => { switch (message.type) { case 'webviewReady': console.log('[Extension] Webview reported ready'); webviewReady = true; // Send initial data AFTER webview is ready panel.webview.postMessage({ type: 'updateAgents', agents: cachedAgents, }); panel.webview.postMessage({ type: 'updateModels', models: cachedModels, }); break; } });
❌ Anti-Pattern: Complex Handshakes
Avoid adding complexity like ping/ACK/retry/fallback mechanisms:
` ypescript // ❌ Bad: Overly complex handshake let webviewReadyAcked = false; let webviewReadyRetryTimer = null; let webviewReadyAttempts = 0;
function startWebviewReadyHandshake() { sendWebviewReady(); webviewReadyRetryTimer = setInterval(() => { if (webviewReadyAcked || webviewReadyAttempts >= 12) { clearInterval(webviewReadyRetryTimer); return; } sendWebviewReady(); // Retry }, 500); }
// Host pings webview, webview responds, host ACKs... // This adds complexity and often fails in unexpected ways `
Why it fails:
- More moving parts = more failure modes
- Race conditions between ping/ACK/retry timers
- Fallback mechanisms mask the real problem
Simple pattern is best: One webviewReady message at script end, host waits for it.
Debugging Tips
1. Host logs vs Webview logs are separate
- Extension host: console.log() appears in Debug Console
- Webview: console.log() appears in Webview Developer Tools
- Use Developer: Open Webview Developer Tools command
2. Verify script execution via message javascript // First line after acquireVsCodeApi vscode.postMessage({ type: 'scriptStarted' }); If host receives this, script is running. If not, check CSP/nonce.
3. Check CSP errors in Webview DevTools
- Open Webview Developer Tools
- Look for CSP violation errors in Console
Webview JavaScript Anti-Patterns
Webview内のJavaScriptは通常のブラウザ環境と異なる動作をする場合があります。以下のパターンを避けてください。
1. アロー関数 vs 従来関数
Webview環境では従来のfunction構文がより安全です:
// ❌ Bad: Arrow function
btn.addEventListener("click", (e) => {
handleClick(e);
});
// ✅ Good: Traditional function
btn.addEventListener("click", function (e) {
handleClick(e);
});2. nullチェック必須
getElementById は null を返す可能性があります。常にnullチェックを行ってください:
// ❌ Bad: No null check
document.getElementById("my-input").value = "xxx";
// ✅ Good: With null check
var element = document.getElementById("my-input");
if (element) element.value = "xxx";3. イベント委譲パターン推奨
NodeListへの直接イベント登録は失敗する可能性があります。イベント委譲を使用してください:
// ❌ Bad: Direct event registration on NodeList
document.querySelectorAll(".btn").forEach(function (btn) {
btn.addEventListener("click", handleClick);
});
// ✅ Good: Event delegation
document.addEventListener("click", function (e) {
var target = e.target;
if (target && target.classList && target.classList.contains("btn")) {
e.preventDefault();
handleClick(target);
}
});4. var を使用
互換性のため、const / let より var を推奨:
// ❌ Bad: const/let
const items = [];
let count = 0;
// ✅ Good: var
var items = [];
var count = 0;5. デフォルト引数の回避
ES6のデフォルト引数構文は避けてください:
// ❌ Bad: Default parameters
function updateOptions(source, selectedPath = "") {
// ...
}
// ✅ Good: Manual default
function updateOptions(source, selectedPath) {
selectedPath = selectedPath || "";
// ...
}6. 初期データの埋め込み
"Loading..."をハードコードせず、初期データがある場合は直接埋め込んでください:
// ❌ Bad: Hardcoded loading state
return `<select id="agent-select">
<option value="">Loading...</option>
</select>`;
// ✅ Good: Embed initial data if available
const options =
agents.length > 0
? agents.map((a) => `<option value="${a.id}">${a.name}</option>`).join("")
: '<option value="">Loading...</option>';
return `<select id="agent-select">${options}</select>`;7. 非同期処理のフォールバック
API呼び出しには常にtry/catchとフォールバックデータを用意してください:
// ❌ Bad: No fallback
async function getModels(): Promise<Model[]> {
return await vscode.lm.selectChatModels({});
}
// ✅ Good: With fallback
async function getModels(): Promise<Model[]> {
try {
const models = await vscode.lm.selectChatModels({});
if (models && models.length > 0) {
return models;
}
} catch {
// API may not be available
}
return getFallbackModels();
}8. data-action + 委譲でアクションを束ねる
// ✅ Good: render attributes, delegate once
function renderTasks(tasks) {
return tasks
.map(function (task) {
var id = escapeAttr(task.id || "");
return '<button data-action="run" data-id="' + id + '">Run</button>';
})
.join("");
}
document.addEventListener("click", function (e) {
var target = e.target;
var host =
target && typeof target.closest === "function"
? target.closest("[data-action]")
: null;
if (!host) return;
var action = host.getAttribute("data-action");
var id = host.getAttribute("data-id");
if (!action || !id) return;
if (action === "run") window.runTask(id);
if (action === "edit") window.editTask(id);
// ... other actions ...
});- ❌ Avoid
onclick="..."直書き(クォート崩れ・minify時のSyntaxErrorの温床)。 - ❌ Avoid TypeScript キャスト文字列(
as HTMLElementがそのままHTMLに出てSyntaxError)。 - ✅ 属性は必ず escape し、委譲で処理する。
9. ビルド後 HTML の健全性チェック
- ビルド時に
debug-webview.htmlを出力し、実ファイルをブラウザ/VS Codeで開いて SyntaxError を確認する。 - Webview Developer Tools の Console を確認し、CSP/quote崩れ/
document.writeなどのエラーを検知する。 - タブ切り替え・プルダウンなど主要動作を1回ずつ手動で触り、ログにエラーが出ないか見る。
正規表現リテラルの二重エスケープ
テンプレートリテラル内で正規表現を記述する際、バックスラッシュが消える問題があります:
// ❌ Bad: Backslash gets stripped in template literal
const html = `<script>var everyN = /^\*\/(\d+)$/.exec(minute);</script>`;
// Result in browser: /^*/(\d+)$/ → SyntaxError: Nothing to repeat
// ✅ Good: Double-escape backslashes
const html = `<script>var everyN = /^\\*\\/(\\d+)$/.exec(minute);</script>`;
// Result in browser: /^\*\/(\d+)$/ → Works correctly影響を受けるパターン:
\d→\\d\s→\\s\*→\\*\/→\\/
デバッグ方法:
1. Webview Developer Toolsを開く 2. Consoleで Invalid regular expression: /^*/: Nothing to repeat を探す 3. ビルド出力 (out/extension.js) で該当の正規表現を確認
設定変更の即時反映
言語設定などを変更した際、Webviewを即座に再レンダリングするパターン:
// extension.ts
const configWatcher = vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration("myExtension.language")) {
// Webviewを新しい言語で再レンダリング
MyWebview.refreshLanguage(getCurrentData());
}
if (
e.affectsConfiguration("myExtension.globalPromptsPath") ||
e.affectsConfiguration("myExtension.globalAgentsPath")
) {
// キャッシュをクリアして再取得
void refreshCachedData(true);
}
});
context.subscriptions.push(configWatcher);// webview.ts
static refreshLanguage(data: any[]): void {
if (this.panel) {
// パネルを閉じて再作成(言語変更を反映)
this.panel.dispose();
this.panel = undefined;
void this.show(this.extensionUri, data, this.onAction);
}
}Moving Inline JS to an External File (Recommended)
Large inline <script> blocks inside TypeScript template literals are hard to edit, cause merge conflicts, and slow down the webview parse step. Prefer an external file.
Anti-pattern (inline)
// BAD – hundreds of lines of JS buried in a TS template literal
return `<html>...
<script nonce="${nonce}">
// 1000 lines of JS here
</script>
</html>`;Preferred pattern (external file)
my-extension/
├── media/
│ └── webview.js ← all webview logic lives here
└── src/
└── myWebview.ts ← only HTML skeleton + initial-data injection// myWebview.ts – only the skeleton remains in TypeScript
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(extensionUri, "media", "webview.js"),
);
return `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none';
style-src ${webview.cspSource} 'unsafe-inline';
script-src 'nonce-${nonce}';
img-src ${webview.cspSource};
font-src ${webview.cspSource};">
</head>
<body>
<script nonce="${nonce}" id="initial-data"
type="application/json">${serializeForWebview(data)}</script>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;// Tighten localResourceRoots to only what is needed
this.panel = vscode.window.createWebviewPanel(
"myWebview",
"My View",
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(extensionUri, "media"), // JS / CSS
vscode.Uri.joinPath(extensionUri, "images"), // icons
// ❌ Don't pass extensionUri directly – too broad
],
},
);Serialising initial data safely
function serializeForWebview(value: unknown): string {
const json = JSON.stringify(value ?? null) ?? "null";
return json
.replace(/</g, "\\u003c")
.replace(/\u2028/g, "\\u2028")
.replace(/\u2029/g, "\\u2029");
}// media/webview.js – read the injected data
(function () {
var vscode = acquireVsCodeApi();
var initialData = {};
try {
var el = document.getElementById("initial-data");
if (el) initialData = JSON.parse(el.textContent || "{}");
} catch (e) {
/* ignore */
}
// … use initialData …
})();Prompting Reload After Extension Update
Because activationEvents: ["onStartupFinished"] fires only once per VS Code startup, users who update the extension without restarting VS Code will keep running stale code. Show a "Reload Now" notification when the version changes.
// extension.ts
const LAST_VERSION_KEY = "lastKnownVersion";
export function activate(context: vscode.ExtensionContext): void {
const currentVersion =
(context.extension.packageJSON as { version?: string }).version ?? "0.0.0";
const lastVersion = context.globalState.get<string>(LAST_VERSION_KEY);
if (lastVersion && lastVersion !== currentVersion) {
void vscode.window
.showInformationMessage(
`Extension updated to v${currentVersion}. Reload to activate.`,
"Reload Now",
)
.then((choice) => {
if (choice === "Reload Now") {
void vscode.commands.executeCommand("workbench.action.reloadWindow");
}
});
}
void context.globalState.update(LAST_VERSION_KEY, currentVersion);
}Why not `vscode.env.reload()`? It reloads immediately without user consent.
The pattern above lets users finish their current work first.