DD Code GUI — VS Code Extension
AI-powered code assistant for VS Code. Dual-engine (Claude Agent SDK + Codex SDK) chat with tool calls, MCP, terminal.
Features
AI Chat
- Dual-engine streaming chat — Claude Agent SDK + Codex SDK with tool calls
- Slash commands —
/help, /clear, /new, /compact, /summarize, /context, /mcp, /skills, /plan, /fix, /tests and more
- @-file mentions — type
@ in chat input to search and reference workspace files
- Multi-agent mode (Codex) — configurable threads, depth, custom agent roles
Code Operations
- Keyboard shortcuts —
Alt+Shift+8 send cursor context, Alt+Shift+9 code edit popup, Escape abort
- Right-click context menu — "DD Code GUI" submenu: Send Selection, Explain, Fix, Optimize, Add Comments
- SCM commit message — sparkle button in Source Control generates Conventional Commits from staged diff
- MCP server integration — 22 marketplace templates + custom stdio/SSE/HTTP servers
- Skill system — 17 marketplace templates + auto-discovery from disk
- CLI tools — auto-detect local tools (npm, mvn, git etc.), live-reload config
- Built-in terminal — full PTY terminal in chat panel
Theme & Language
- Dark & Light themes — 42 CSS variable pairs, adaptive UI (scrollbars, code blocks, tool panels, terminal)
- Internationalization — Chinese/English UI, instant switch
Engine Bridge
- Moon Bridge — local HTTP proxy for DeepSeek/Anthropic-compatible API via Codex, port auto-increment
- Flash-First model routing — auto-select flash/pro model based on task complexity
Project Integration
- Multi-window safety — advisory file locks prevent session corruption across VS Code windows
- Project-level settings — settings default to Workspace scope when a workspace is open
- Global settings — API keys and endpoints shared across projects
Quick Start
cd vscode-extension && npx vsce package
vscode:prepublish auto-runs: tsc → copy-bridge → build-webview → VSIX.
Install: VS Code Extensions → ... → Install from VSIX...
Architecture
React SPA (webview/) ← zero changes, same codebase as IntelliJ
↕ postMessage
VS Code Extension (TypeScript) ← replaces Java glue layer
↕ stdin/stdout NDJSON
Node.js Daemon (claude-bridge/) ← zero changes
Project Structure
src/
extension.ts # activate/deactivate, ~70 webview message cases, 12 commands, context menu
daemon/
daemon-manager.ts # spawn, heartbeat (15s), crash recovery (max 3, recoveryLock)
ndjson-stream.ts # NDJSON line reader/writer (readline + JSON.parse)
webview/
webview-provider.ts # WebviewView provider, bridge script, CSP, message buffering
handlers/
chat-handler.ts # Chat streaming, message accumulation (mirrors ChatSendHandler)
session-manager.ts # Session persistence (.claude/sessions/), chunked loading, advisory locks
config-handler.ts # MCP/Skills/CLI Tools CRUD + disk scanning
settings-handler.ts # Claude/Codex/Global settings with debounced config sync, Workspace/Global scope
attachment-handler.ts # File attachment + temp file staging
commit-handler.ts # Git commit message generation (SCM button, diff collection)
mcp-handler.ts # MCP server management
skill-handler.ts # Skill management + auto-discovery
clitool-handler.ts # CLI Tool management + env-scanner import
terminal-handler.ts # Terminal integration (spawn, resize, input)
context-handler.ts # Project context extraction
mcp/
editor-state-server.ts # TCP server (port 0) — 4 tools for idea-state-mcp-server.js
types/
messages.ts # All daemon/webview message type definitions
resources/
bridge/ # 28 JS files from claude-bridge/ (copied at build)
webview/ # Vite-built index.html (IIFE, single-file inlined)
mcp-marketplace.json # 22 MCP server templates
skills-marketplace.json # 17 skill templates
Configuration
All settings under deepseek-chat.* namespace. Settings default to Workspace scope when a folder is open, falling back to Global. API keys are stored as plaintext in VS Code configuration.
| Setting |
Default |
Description |
claude.provider |
DEEPSEEK |
Provider: DEEPSEEK, ANTHROPIC, LM_STUDIO, OLLAMA, LOCAL, CUSTOM |
claude.endpoint |
https://api.deepseek.com |
Custom API endpoint (empty = provider default) |
claude.apiKey |
"" |
API key (stored in VS Code config) |
claude.model |
"" |
Model ID (empty = Auto) |
claude.permissionMode |
default |
Tool permission: default, acceptEdits, plan, bypassPermissions |
claude.thinkingEnabled |
true |
Enable extended thinking |
claude.multiAgentEnabled |
false |
Allow model to spawn sub-agents (Task/Agent). DeepSeek provider does not support sub-agents |
claude.thinkingDepth |
high |
Thinking depth: low, medium, high, xhigh, max |
claude.responseLanguage |
ENGLISH |
Response language preference |
codex.provider |
OPENAI |
Codex provider: OPENAI, CUSTOM |
codex.endpoint |
https://api.openai.com |
Custom Codex endpoint |
codex.apiKey |
"" |
API key (stored in VS Code config) |
codex.model |
"" |
Model ID (empty = Auto) |
codex.responseLanguage |
ENGLISH |
Response language for Codex |
codex.maxThreads |
6 |
Max parallel threads for multi-agent |
codex.maxDepth |
1 |
Max agent delegation depth |
codex.multiAgentEnabled |
false |
Enable multi-agent mode (needs a spawn-capable provider; DeepSeek not supported) |
codex.moonBridgePort |
38440 |
Moon Bridge proxy port (auto-increments on conflict) |
codex.cliPath |
"" |
Custom CLI path for Codex SDK |
global.claudeApiKey |
"" |
Default Claude API key (shared across projects) |
global.claudeEndpoint |
"" |
Default Claude endpoint override |
global.codexApiKey |
"" |
Default Codex API key (shared across projects) |
global.codexEndpoint |
"" |
Default Codex endpoint override |
global.codexCliPath |
"" |
Custom Codex CLI path (global default) |
global.codexMoonBridgePort |
38440 |
Default Moon Bridge port (global default) |
Custom Agent Roles
Codex settings panel supports custom agent roles. Roles persist in workspace state (not settings.json) and sync to the daemon automatically.
| Field |
Required |
Description |
name |
Yes |
Short display name |
description |
Yes |
Role behavior description |
model |
No |
Override model for this role |
reasoningEffort |
No |
Reasoning effort: medium or high |
Key Design Patterns
Webview Bridge
- CSP compliance: No inline scripts. Vite
vite-plugin-singlefile inlines JS/CSS into single HTML; extension extracts script bodies to external app.js/bridge.js. Both under localResourceRoots.
- Message buffering:
Object.defineProperty(window, 'onMessage', {set}) traps assignment. Messages arriving before React mounts → buffered in _pendingMessages[] → replayed when window.onMessage is set. Prevents ErrorBoundary crash from missing initial data.
retainContextWhenHidden: true: Prevents React state loss when user switches tabs away from chat view.
Settings Sync
- Debounced config listener:
subscribeToChanges() debounces onDidChangeConfiguration 300ms. updateClaudeSettings() loops per-key config.update() — each fires event → debounce coalesces into single callback → one sendCommand to daemon.
- Split-path sync (non-key vs. apiKey):
onDidChangeConfiguration callback does NOT await SecretStorage — the callback is not awaited by VS Code, so async work interleaves with webview messages. During the await gap, fetch_models can race ahead of the settings update. Fix: non-key fields (endpoint, model, provider) sent IMMEDIATELY; apiKey resolved from SecretStorage async in background via (async()=>{...})().catch(()=>{}). Masked placeholder '••••••••' from VS Code config is rejected — never overwrites the real key delivered by init.
- SecretStorage: API keys stored via
context.secrets (legacy — migrated to plaintext VS Code config in 1.0.2)
- sendInitialData ordering: Data sent BEFORE daemon
ready. Settings populated in React state before daemon_status: "ready" arrives.
- Scope:
updateClaudeSettings()/updateCodexSettings() default to ConfigurationTarget.Workspace when a workspace is open, Global otherwise.
Session Persistence (Multi-Window Safe)
- Advisory file locks:
acquireLock() uses fs.writeFileSync(path, pid, {flag: 'wx'}) — exclusive-create semantics. PID stamped inside lock file, 30s staleness detection handles orphan locks from crashed processes.
- Atomic writes:
.tmp + fs.renameSync() ensures readers never see partial writes.
- Merge on save:
saveIndex() and saveSession() re-read on-disk state under lock, merge new entries from other windows before writing. Prevents lost updates in multi-window scenarios.
- Version tracking:
sessions.json stores {version, entries} format. Backward-compatible with old plain-array format via Array.isArray() detection.
- Session revision: Each
saveSession() increments revision field — enables conflict detection.
Must match Java buildMessagesFromAccumulation() exactly:
- User:
{role:'user', content:<string>, timestamp:<number>}
- Assistant:
{role:'assistant', content:[{type:'thinking',thinking}, {type:'text',text}, {type:'tool_use',id,name,input}], timestamp}
- Tool results: SEPARATE
{role:'user', content:[{type:'tool_result',tool_use_id,content,is_error}]} message — NOT embedded in tool_use blocks. Webview searches for these to populate tool cards.
normalizeMessages() in session-manager.ts handles old-format upgrades.
Daemon Lifecycle
- Init ordering: spawn → send init → start stdout reader. Critical — init must write stdin before reader starts.
- Deaf-state detection:
stdoutReaderAlive flag. stdout reader close → force crash recovery. Prevents silent failure when pipe breaks but process lives.
- Double reader prevention: Only
restartProcess() creates NdjsonReader. Callers must not duplicate — two readline interfaces on same stream compete for lines.
- Platform-aware cleanup: Windows
process.kill() (TerminateProcess), Unix process.kill('SIGKILL').
- Log ring buffer: Last 500 daemon log lines retained via
logBuffer[], exposed via getRecentLogs() for devtools.
Slash Commands
Format: {name, descZh, descEn, local} objects, NOT string[]. ChatInput.tsx:164 calls c.name.slice(1) — if c is string, c.name is undefined → ErrorBoundary crash. buildSlashCommands() in extension.ts mirrors SlashCommandRegistry.java. Defensive normalization in webview main.tsx handles legacy string[].
Editor State MCP
TCP ServerSocket on port 0. 4 tools: get_open_file_paths, get_current_file_info, get_editor_context, get_selection_text. Port injected into daemon init as mcpServers["idea-state"]. sendFullMcpConfig() dual-coordinates — fires from both daemon ready AND server.start().then(), one-shot via mcpConfigSent flag.
Moon Bridge Port Auto-Increment
- Port probing:
probePort() uses net.createServer().listen(port) to check availability before spawning the Go binary.
- Auto-increment: If the preferred port is busy, probes up to 10 subsequent ports (38440 → 38441 → … → 38449).
- Port back-sync: When daemon resolves a different port, it emits
moon_bridge_status with port. Extension forwards to webview → SET_CODEX_MOON_BRIDGE_PORT updates the UI.
- TOCTOU accepted: Probe-use window is milliseconds; if another process grabs the port during that window, the Go binary fails to bind and Moon Bridge reports error status. No corruption risk.
Engine Switch (Provider Switch)
- Full flow:
SettingsSidebar.tsx → sendToJava('switch_provider', {messages, responseLanguage}) → extension.ts → compact_request({messages, provider, options:{responseLanguage}}) → daemon
- 6 requirements: (1)
abort before compact resets streaming, (2) chatHandler.resetStreaming() clears loading flag, (3) messages from frontend state (last 50), (4) responseLanguage from old engine so summarization uses correct language, (5) non-key settings sent immediately after compact, (6) apiKey resolved from SecretStorage async in background
- Frontend banner:
COMPACT_DONE reducer checks window.__switchProviderMeta → shows "引擎已切换" with optional warning text when compact fails
Summary Anti-Hallucination Validation
validateSummary(summary, conversationText) shared by both codex-sdk and agent-sdk
extractKeyTerms(text) extracts up to 30 terms: filenames from paths, CamelCase identifiers (>6 chars), snake_case (3+ segments), Chinese phrases (2-4 chars). No terms found → auto-pass
- Validation gate:
ratio < 0.15 && matchCount < 3 → low_overlap (fatal). 3+ exact keyword matches always pass. model_rejected → returns null (non-fatal, shows "对话太短")
- Codex
summarizeText() MUST filter content blocks to text/thinking/reasoning/output_text types only — without filtering, tool_use blocks become empty strings, flooding the 15000-char window and pushing technical terms out
responseLanguage MUST be in compact_request.options — without it, defaults to 'ENGLISH' → Chinese conversations summarized in English → Chinese terms don't match → low_overlap
resolveUpstreamEndpoint() (codex-sdk): Provider checks (DEEPSEEK/OPENAI) MUST precede CODEX_BASE_URL check. Prevents stale CODEX_BASE_URL=https://api.openai.com from short-circuiting before provider === 'DEEPSEEK'
Streaming Context Key
deepseek-chat.streaming set true on send_message, false on daemon done/aborted/error. Escape keybinding uses "when": "deepseek-chat.streaming" for conditional abort.
Additional Patterns
- Crash recovery:
recoveryLock prevents concurrent recovery. recoveryInProgress flag keeps loading: true during send_message restart. stream_resumed sent instead of daemon_status: "ready".
- Send idempotency:
lastSentText/lastSentSessionId/lastSentTime tracked. During recovery, matching send within DEDUP_WINDOW_MS=120000 suppressed.
aborted accumulator reset: chat-handler.ts resets accumulators on aborted — prevents stale data leaking into next stream.
- Skill auto-discovery: Scans
.claude/skills/, ~/.claude/skills/, ~/.codemoss/skills/ for *.md files at startup.
- Right-click UX:
webviewProvider.ensureVisible() shows view without toggling (VS Code workbench.view.extension.* is a toggle). File tag chips show line numbers only for selections, not whole files. set_chat_input_text no longer sends @file:xxx (redundant with chips).
- MCP test Windows:
cp.spawn doesn't resolve .cmd/.bat on Windows. Bare commands appended .cmd, then wrapped with cmd.exe /c (mirrors mcp-bridge-server.js).
/context command: MUST include mcpServers (array of {name, type, disabled}) and skills (array of {name, displayName, enabled, description}) plus pendingSummary from session. Without these, daemon defaults to empty arrays → MCP/Skill counts always 0.
atob() safety: Attachments may contain non-base64 data (e.g., raw file content). SDK internally calls atob() which throws InvalidCharacterError. Validate with BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/ before atob(); fallback to Buffer.from(data, 'utf-8').toString('base64').
syncLock compact unblock: daemon.js enqueue inserts a syncLock blocker when compact/summarize is queued — prevents concurrent claude_settings_update / codex_settings_update from overwriting env vars mid-compact. Resolved in channel-manager.js finally block via cmd._syncUnblock(). Without this, compact reads stale or wrong env vars.
- Codex multi-agent off → single thread: When
multiAgentEnabled === false, codex-sdk-service.js forces agents.max_threads: 1 and agents.max_depth: 0. Without this, SDK defaults to multi-agent behavior even when toggle is off.
- Daemon stderr tee:
daemon.js monkey-patches process.stderr.write to also write to <project>/.claude/daemon/daemon.log. Early writes (before init) buffered in memory, flushed when project path is known. Best-effort; errors silently ignored.
Requirements
- VS Code
^1.85.0
- Node.js >= 18 for daemon
- Agent SDK auto-installed to
~/.codemoss/dependencies/
Keyboard Shortcuts
| Shortcut |
Command |
When |
Alt+Shift+8 |
Send selection / cursor context to chat |
editorTextFocus |
Alt+Shift+9 |
Code edit operations (Explain/Fix/Optimize/Add Comments) |
editorTextFocus |
Escape |
Abort current AI generation |
deepseek-chat.streaming |
Editor context menu → "DD Code GUI" submenu (when editor is open):
| Command |
When |
Behavior |
| Send Selection to DD Code Chat |
Always |
Selection → selected text with line numbers. No selection → cursor context (prefix 2500 + <CURSOR> + suffix 1200 chars) |
| Explain Code with DD Code |
Selection |
Pre-fills "解释以下代码" / "Please explain..." |
| Fix Code with DD Code |
Selection |
Pre-fills "修复以下代码中的问题" / "Please fix bugs..." |
| Optimize Code with DD Code |
Selection |
Pre-fills "优化以下代码" / "Please optimize..." |
| Add Comments with DD Code |
Selection |
Pre-fills "为以下代码添加注释" / "Please add comments..." |
Explorer context menu → "Send File to DD Code Chat". Prompt language switches based on responseLanguage setting.
Git Commit Message (SCM)
Click the sparkle button in the Source Control titlebar (or run DD Code GUI: Generate Commit Message with AI). The extension:
- Runs
git diff --cached (falls back to unstaged changes if nothing staged)
- Sends the diff to the AI with a Conventional Commits format prompt
- Sets the chat input — submit and the AI generates the message, which can be copied to the SCM input box
Debug
- Output panel: "DD Code GUI" (View → Output, select from dropdown)
- Daemon logs:
[daemon] prefix in Output panel + ring buffer (getRecentLogs)
- Debug log file:
~/.claude/vscode-debug.log (extension logs only)
- Daemon log file:
<project>/.claude/daemon/daemon.log (daemon stderr tee, includes resolveUpstreamEndpoint, summarizeText, validation diagnostics). Search summarizeText convTextLen= / terms= / summarization for compact debugging.
- Sessions:
~/.claude/sessions/
- Permissions:
~/.claude/daemon-permissions.json
- Dev extension: F5 → Extension Development Host
| |