- OS: Windows 10.0.19045
- Shell: Git Bash
- Path format: Windows (use forward slashes in Git Bash)
- File system: Case-insensitive
- Line endings: CRLF (configure Git autocrlf)
Compile:
npm run compilePackage VSIX (must use cmd wrapper, Git Bash swallows vsce output):
cmd //c "npx @vscode/vsce package --no-dependencies"--no-dependencies: skipsnpm installduring packaging — dependencies are already innode_modulesfrom development; without this flag, vsce may fail or produce a bloated package- Do NOT use
npx @vscode/vsce packagedirectly in Git Bash — it silently fails (exit 0 but no .vsix generated) - Output file:
claude-code-chatui-{version}.vsix
Install VSIX for testing:
- VS Code:
Ctrl+Shift+P→ "Install from VSIX" - CLI:
code --install-extension claude-code-chatui-{version}.vsix
Debug (Extension Development Host):
Ctrl+Shift+D→ select "Run Extension" → click green play button- Remote desktop: F5 may be intercepted, use the play button instead
User input → Webview postMessage → ClaudeChatProvider
→ ClaudeProcessService (stdin JSON) → Claude CLI
→ stdout JSON stream → MessageProcessor → postMessage → Webview
| Component | File | Role |
|---|---|---|
| Entry point | src/extension.ts |
Registers commands, subscriptions, status bar |
| Webview orchestrator | src/providers/ClaudeChatProvider.ts |
Owns all managers/services, handles all webview messages |
| CLI lifecycle | src/services/ClaudeProcessService.ts |
Spawn, kill, temp-file cleanup |
| Stream parser | src/services/MessageProcessor.ts |
JSON-line parsing, tool-use extraction, token/cost dispatch |
| Process mgmt | src/managers/WindowsCompatibility.ts |
Executable discovery, taskkill tree kill, shell env |
| Config facade | src/managers/config/ConfigurationManagerFacade.ts |
Combines VsCode + MCP + API config managers |
| Undo/redo | src/managers/UndoRedoManager.ts |
Strategy pattern — one strategy class per operation type |
| UI HTML | src/ui-v2/index.ts |
Assembles full HTML: CSP header + styles + body + script |
| UI script | src/ui-v2/ui-script.ts |
Entire frontend JS as a TypeScript template literal |
| UI body | src/ui-v2/getBodyContent.ts |
HTML body markup (settings panel, chat area, footer) |
index.ts calls getBodyContent() for the HTML body and getScript() (from ui-script.ts) for the frontend JS, then wraps them in a complete HTML document with a CSP <meta> tag and <style> block. The result is a single self-contained HTML string — no external resources are loaded.
- Strategy pattern: Undo/redo operations — each
OperationTypehas a strategy insrc/managers/operations/strategies/ - Facade pattern:
ConfigurationManagerFacadeunifies 3 config sub-managers - Singleton pattern:
DebugLogger,PluginManager,SkillManager,SecretService - Stream protocol: CLI communication via
--input-format stream-json --output-format stream-json
The webview has 119 inline event handlers (onclick, onchange, etc.) spread across getBodyContent.ts (~98) and ui-script.ts (~21). Any CSP policy using script-src 'nonce-xxx' or script-src 'strict-dynamic' will freeze the entire UI — buttons become unresponsive, no errors in console.
Current policy (src/ui-v2/index.ts):
default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src 'none';
Rule: Do NOT attempt nonce-based CSP unless you first refactor all 119 inline handlers to addEventListener. This has caused production breakage twice.
ui-script.ts exports a JavaScript string inside a TypeScript template literal. This creates double-layered escaping:
- Source
\\\\→ JS output\\→ runtime\ - Source
\\'→ JS output' - Template literals inside the JS code use ``` (escaped backtick)
When writing regex or escape sequences in ui-script.ts, always think: "What does the TypeScript compiler emit, and what does the browser's JS engine see?"
Example — matching a single backslash in the browser:
Source (ui-script.ts): str.replace(/\\\\\\\\/g, ...) // 4 backslashes in source
TS compiler emits: str.replace(/\\\\/g, ...) // 2 backslashes in JS
Browser regex matches: \ // 1 literal backslash
ui-script.ts dynamically builds HTML via string concatenation. All user-controlled data must be escaped:
- Display text:
escapeHtml(value) - Inside
onclickattributes:escapeForOnclick(value)(JS-escape then HTML-escape) - Markdown content:
escapeHtml()first, then pass toparseSimpleMarkdown()
Windows does NOT auto-kill child processes when a parent exits (unlike Linux SIGHUP). Both scenarios create orphans:
- Closing the chat panel (webview dispose)
- Closing VS Code entirely
Fix: ClaudeProcessService.dispose() calls killProcess(pid) which uses taskkill /t /f to kill the entire process tree. Both provider and treeDisposable must be in context.subscriptions to ensure dispose() fires on VS Code exit.
npx @vscode/vsce package silently produces no output in Git Bash (exit code 0 but no .vsix file). Always wrap with cmd //c "...".
The cache uses two separate timestamps:
fileTimestamp: the file's mtime — detects if the file changed on diskcachedAt: when the cache entry was created — drives the 5-minute TTL expiry
Before v3.1.9, these were a single field, causing all caches for files older than 5 minutes to be perpetually "expired."
When building Stop-hook completion notifications on Windows, use the WinRT Toast API (not MessageBox, not notify-send). Two places use this pattern and must stay aligned:
- User's personal hook script:
~/.claude/hooks/stop-notify.ps1(standalone.ps1file referenced from the user'ssettings.json) - Plugin's built-in template:
buildWindowsToastNotifyCmd()insrc/services/HooksConfigManager.ts(embedded in a TS template literal, base64-encoded forpowershell -EncodedCommand)
Anatomy of a Toast — two separate icon slots:
- Top-left small icon (next to the app name): controlled by
IconUriregistry value underHKCU:\Software\Classes\AppUserModelId\<appId>. To get the minimalist default square glyph (⊞), do NOT setIconUri(or delete it if set). Setting it to a PNG will show that PNG scaled small, which is often not what you want. - Body image on the left (large): controlled by
<image placement="appLogoOverride" src="file:///...">inside the Toast XML. Located by searching$env:CLAUDE_PROJECT_DIR\icon.pngthen$PWD\icon.png. If not found, omit the<image>element entirely.
AppUserModelId cache trap — Windows caches icons per AppUserModelId in its notification database. Deleting IconUri from the registry does NOT always refresh the displayed icon; the old one is stuck in the cache. To force a fresh look: change $appId to a new string (Windows treats it as a brand-new app with no cache) and delete the old registry key. Restarting explorer.exe sometimes works but is unreliable.
Sound — the XML's <audio src="ms-winsoundevent:Notification.Default" /> goes through the Toast audio pipeline, which can be silenced by Windows Focus Assist or per-app notification sound settings. Always add a fallback: try { [System.Media.SystemSounds]::Asterisk.Play() } catch {} right before .Show($toast). This uses a different audio pipeline and works independently.
App name — register DisplayName = 'Claude Code' on the AppUserModelId; otherwise the top-left shows "Windows PowerShell".
Anti-loop guard — parse stdin JSON for stop_hook_active; if true, exit 0 without firing the notification. Otherwise the hook triggers itself recursively when Claude responds to the hook's output.
Embedding in TypeScript — for the plugin template, write the PowerShell as a clean multi-line TS template literal, then at runtime: Buffer.from(script, 'utf16le').toString('base64') and invoke as powershell -NoProfile -EncodedCommand ${b64}. This avoids the nested-quote/backslash escaping nightmare of a single-line inline command.
The context-window indicator and auto-compaction are controlled by CLI-side behavior, not the plugin's own math. Reverse-engineered from CLI 2.1.85 (ClaudeProcessService.ts injects these):
[1m]model suffix — appending[1m]to the model ID (e.g.--model "claude-fable-5[1m]") forces the CLI to treat the window as exactly 1,000,000 tokens regardless of whether it recognizes the model. The CLI detects it via/\[1m\]/i.test(model). Without it, unrecognized new models (fable-5, sonnet-5) default to a 200K window, causing the indicator to stall at ~16-18% and premature auto-compaction (~82.5% ≈ 165K observed).CLAUDE_CODE_AUTO_COMPACT_WINDOW— sets the auto-compaction trigger point, but the CLI applies it withMath.minagainst the model's real window: it can only SHRINK the window, never GROW it. So it only takes effect when combined with[1m](which raises the ceiling to 1M first). Current setting:400000→ effective compaction ≈ 367K (400K minus ~33K output reserve).compact_boundarymessage — CLI 2.1.85 emits this system message on compaction; older handling silently dropped it, so the frontend's "indicator only increases, never decreases" logic never reflected post-compact drops.MessageProcessormust handle it.
Rule: When adding a new model that the installed CLI may not recognize, always ship it with the [1m] suffix + CLAUDE_CODE_AUTO_COMPACT_WINDOW, otherwise the context indicator breaks. The MODEL_CONTEXT_WINDOWS table in constants.ts is the plugin-side source of truth for display.
When bumping the version, update all five locations:
package.json→"version": "x.y.z"src/ui-v2/getBodyContent.ts→ version display string (search forvX.Y.Z)CHANGELOG.md→ add new version section at the topREADME.md→ add row at the top of the "Recent Updates" tableREADME.zh-CN.mdandREADME.zh-TW.md→ same table, localized text
Then:
npm run compile
cmd //c "npx @vscode/vsce package --no-dependencies"Verify the output file name matches the new version: claude-code-chatui-{version}.vsix
After packaging, publish the release on GitHub:
- Create a new Release tag
vX.Y.Zpointing to the latest commit onmain - Paste the CHANGELOG section as the release body
- Upload
claude-code-chatui-{version}.vsixas the release asset
- User communication: Chinese — conversations with the maintainer, PR descriptions, issue comments
- Code comments: English only
- Spec naming:
specs/{topic}.mdfor requirements,specs/{topic}-PLAN.mdfor implementation plans - Commit messages: can be Chinese or English, but code-facing content (comments, variable names, log strings) must be English
- No unused dependencies: remove from
package.jsonif no code references exist .vscodeignore: keepspecs/**,reference/**,CCimages/**,.claude/**excluded from VSIX — these are dev-only; including them bloats the VSIX with no user benefit- Tests: no automated test suite currently exists; verification is done manually via the Extension Development Host (F5)
- Linting:
eslint.config.mjsis present but no pre-commit hooks are configured; runnpx eslint src/manually before packaging
File paths:
- Screenshots:
./CCimages/screenshots/ - PDFs:
./CCimages/pdfs/
Browser version fix:
- Error: "Executable doesn't exist at chromium-XXXX" → Version mismatch
- v1.0.12+ uses Playwright 1.57.0, requires chromium-1200 with
chrome-win64/structure - Quick fix:
npx playwright@latest install chromium - Manual symlink (if needed):
cd ~/AppData/Local/ms-playwright && cmd //c "mklink /J chromium-1200 chromium-1181"
Codex is an autonomous coding agent by OpenAI, integrated via MCP.
Workflow: Claude plans architecture → delegate scoped tasks to Codex → review results
codextool: start a session with prompt, sandbox, approval-policycodex-replytool: continue a session by threadId for multi-turn tasks- Pass project context via
developer-instructionsparameter - Recommended: sandbox='workspace-write', approval-policy='on-failure'
Prerequisite: npm i -g @openai/codex, OPENAI_API_KEY configured