feat: add VSCode extension for Hermes Agent

Add a VSCode extension providing integrated chat interface,
syntax highlighting, and seamless interaction with Hermes Agent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
玉冰 2026-04-25 00:39:28 +08:00
parent e5d41f05d4
commit 91a6d24464
9 changed files with 2788 additions and 0 deletions

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -0,0 +1,139 @@
{
"name": "hermes-vscode",
"displayName": "Hermes Agent",
"description": "Hermes AI Agent for VS Code: Self-improving AI coding assistant",
"version": "0.1.0",
"publisher": "hermes-agent",
"engines": {
"vscode": "^1.94.0"
},
"categories": ["AI", "Chat"],
"keywords": ["hermes", "ai", "coding-assistant", "agent", "mcp"],
"activationEvents": ["onStartupFinished"],
"icon": "resources/hermes-logo.png",
"main": "./extension.js",
"contributes": {
"configuration": {
"title": "Hermes Agent",
"properties": {
"hermes.cliCommand": {
"type": "string",
"default": "hermes",
"description": "Command to invoke the Hermes CLI"
},
"hermes.workingDirectory": {
"type": "string",
"default": "",
"description": "Working directory for CLI (defaults to workspace root)"
},
"hermes.includeActiveFile": {
"type": "boolean",
"default": true,
"description": "Automatically include the active file as context"
},
"hermes.preferredLocation": {
"type": "string",
"enum": ["sidebar", "panel"],
"enumDescriptions": ["Sidebar (Right)", "Panel (New Tab)"],
"default": "panel",
"description": "Where Hermes opens by default"
},
"hermes.autosave": {
"type": "boolean",
"default": true,
"description": "Automatically save files before Hermes reads or writes them"
},
"hermes.useCtrlEnterToSend": {
"type": "boolean",
"default": false,
"description": "Use Ctrl/Cmd+Enter to send prompts instead of just Enter"
},
"hermes.environmentVariables": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "Environment variable name" },
"value": { "type": "string", "description": "Environment variable value" }
},
"required": ["name", "value"]
},
"default": [],
"description": "Environment variables to set when launching Hermes"
}
}
},
"commands": [
{
"command": "hermes.open",
"title": "Hermes: Open",
"icon": { "light": "resources/hermes-logo.svg", "dark": "resources/hermes-logo.svg" }
},
{
"command": "hermes.openInTab",
"title": "Hermes: Open in New Tab",
"icon": { "light": "resources/hermes-logo.svg", "dark": "resources/hermes-logo.svg" }
},
{
"command": "hermes.openInSidebar",
"title": "Hermes: Open in Side Bar",
"icon": { "light": "resources/hermes-logo.svg", "dark": "resources/hermes-logo.svg" }
},
{
"command": "hermes.newConversation",
"title": "Hermes: New Conversation"
},
{
"command": "hermes.focus",
"title": "Hermes: Focus input"
},
{
"command": "hermes.stop",
"title": "Hermes: Stop generation"
}
],
"keybindings": [
{
"command": "hermes.focus",
"key": "cmd+escape",
"mac": "cmd+escape",
"win": "ctrl+escape",
"linux": "ctrl+escape",
"when": "editorTextFocus"
},
{
"command": "hermes.openInTab",
"key": "cmd+shift+escape",
"mac": "cmd+shift+escape",
"win": "ctrl+shift+escape",
"linux": "ctrl+shift+escape"
}
],
"viewsContainers": {
"activitybar": [
{
"id": "hermes-sidebar",
"title": "Hermes Agent",
"icon": "resources/hermes-logo.svg"
}
]
},
"views": {
"hermes-sidebar": [
{
"type": "webview",
"id": "hermes.chatView",
"name": "Hermes Agent"
}
]
},
"menus": {
"editor/title": [
{
"command": "hermes.openInTab",
"group": "navigation"
}
]
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

View file

@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Hermes</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#4A90D9" fill-rule="nonzero"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 223 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 209 KiB

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,543 @@
// @ts-check
// Hermes Agent - Webview Script (Full Feature Parity)
const vscode = acquireVsCodeApi();
const S = {
isGenerating: false,
streamingText: '',
currentAssistantEl: null,
messages: [],
settings: {},
activeFile: null,
currentModel: 'Default',
permissionMode: 'default',
sessionId: null,
pendingPermission: null,
mcpServers: /** @type {Array<{name:string,status:string}>} */ ([]),
};
const $ = (id) => document.getElementById(id);
const $messages = $('messages');
const $messagesContainer = $('messages-container');
const $welcomeScreen = $('welcome-screen');
const $userInput = $('user-input');
const $sendBtn = $('send-btn');
const $stopBtn = $('stop-btn');
const $newChatBtn = $('new-chat-btn');
const $newChatBtnTop = $('new-chat-btn-top');
const $toolProgress = $('tool-progress');
const $toolProgressText = $toolProgress?.querySelector('.tool-progress-text');
const $contextBadge = $('context-badge');
const $contextFilename = $('context-filename');
const $contextDismiss = $('context-dismiss');
const $modelBtn = $('model-btn');
const $modelLabel = $('model-label');
const $sessionsBtn = $('sessions-btn');
const $settingsBtn = $('settings-btn');
const $modeBtn = $('mode-btn');
const $permissionBar = $('permission-bar');
const $permAllow = $('perm-allow');
const $permDeny = $('perm-deny');
// ============================================================
// Markdown
// ============================================================
function md(text) {
if (!text) return '';
let h = esc(text);
h = h.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) =>
`<div class="code-block" data-lang="${lang}"><div class="code-header"><span class="code-lang">${lang || 'text'}</span><div class="code-actions"><button class="code-btn copy-btn" onclick="copyCode(this)">Copy</button><button class="code-btn apply-btn" onclick="applyCode(this)">Apply</button></div></div><pre><code>${code}</code></pre></div>`);
h = h.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
h = h.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
h = h.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
h = h.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="#" onclick="openUrl(\'$2\');return false" class="chat-link">$1</a>');
h = h.replace(/^### (.+)$/gm, '<h4 class="chat-h">$1</h4>');
h = h.replace(/^## (.+)$/gm, '<h3 class="chat-h">$1</h3>');
h = h.replace(/^# (.+)$/gm, '<h2 class="chat-h">$1</h2>');
h = h.replace(/^- (.+)$/gm, '<li>$1</li>');
h = h.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul>$1</ul>');
h = h.replace(/<\/ul>\s*<ul>/g, '');
h = h.replace(/\n\n/g, '</p><p>');
h = `<p>${h}</p>`;
h = h.replace(/<p><\/p>/g, '');
h = h.replace(/\n/g, '<br>');
return h;
}
function esc(t) { return t.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[m])); }
window.copyCode = (btn) => { const c = btn.closest('.code-block')?.querySelector('code')?.textContent || ''; navigator.clipboard.writeText(c).then(() => { btn.textContent = 'Copied!'; setTimeout(() => btn.textContent = 'Copy', 2000); }); };
window.applyCode = (btn) => { const c = btn.closest('.code-block')?.querySelector('code')?.textContent || ''; vscode.postMessage({ type: 'applyEdit', content: c, filePath: '' }); };
window.openUrl = (url) => { vscode.postMessage({ type: 'open_url', url }); };
// ============================================================
// Messages
// ============================================================
function addUserMessage(content) {
hideWelcome();
const el = document.createElement('div');
el.className = 'message message-user';
el.innerHTML = `<div class="message-avatar avatar-user"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="5" r="3"/><path d="M2 14c0-3.3 2.7-6 6-6s6 2.7 6 6"/></svg></div><div class="message-body"><div class="message-role">You</div><div class="message-content">${md(content)}</div></div>`;
$messages.appendChild(el);
S.messages.push({ role: 'user', content });
scrollBottom();
}
function createAssistantEl() {
hideWelcome();
const el = document.createElement('div');
el.className = 'message message-assistant';
el.innerHTML = `<div class="message-avatar avatar-assistant"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg></div><div class="message-body"><div class="message-role">Hermes</div><div class="message-content"><span class="streaming-cursor"></span></div></div>`;
$messages.appendChild(el);
scrollBottom();
return el;
}
function updateAssistant(el, text) {
const c = el?.querySelector('.message-content');
if (!c) return;
c.innerHTML = md(text) + '<span class="streaming-cursor"></span>';
scrollBottom();
}
function finalizeAssistant(el, text) {
const c = el?.querySelector('.message-content');
if (!c) return;
c.innerHTML = md(text);
scrollBottom();
}
function addToolMessage(name, status) {
const el = document.createElement('div');
el.className = 'message message-tool';
el.innerHTML = `<div class="message-avatar avatar-tool"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M14.12 1.88a3 3 0 00-4.24 0L1.59 10.17a2 2 0 00-.52 1.02L.05 14.83a.5.5 0 00.62.62l3.64-1.02a2 2 0 001.02-.52l8.29-8.29a3 3 0 000-4.24z"/></svg></div><div class="message-body"><div class="tool-header"><span class="tool-name">${esc(name || 'Tool')}</span><span class="tool-status">${status || 'running'}</span></div></div>`;
$messages.appendChild(el);
scrollBottom();
}
function addErrorMessage(content) {
const el = document.createElement('div');
el.className = 'message message-error';
el.innerHTML = `<div class="message-avatar avatar-error"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="7"/><path d="M8 4v5M8 11v1" stroke="white" stroke-width="1.5"/></svg></div><div class="message-body"><div class="message-role">Error</div><div class="message-content">${esc(content)}</div></div>`;
$messages.appendChild(el);
scrollBottom();
}
function addResultMessage(cost, duration, turns) {
const parts = [];
if (cost) parts.push(`Cost: $${cost.toFixed(4)}`);
if (duration) parts.push(`Time: ${(duration / 1000).toFixed(1)}s`);
if (turns) parts.push(`Turns: ${turns}`);
if (!parts.length) return;
const el = document.createElement('div');
el.className = 'message message-result';
el.innerHTML = `<div class="result-meta">${parts.join(' &middot; ')}</div>`;
$messages.appendChild(el);
scrollBottom();
}
// ============================================================
// UI Helpers
// ============================================================
function hideWelcome() { if ($welcomeScreen) $welcomeScreen.style.display = 'none'; if ($messagesContainer) $messagesContainer.style.display = 'flex'; }
function showWelcome() { if ($welcomeScreen) $welcomeScreen.style.display = 'flex'; if ($messagesContainer) $messagesContainer.style.display = 'none'; }
function scrollBottom() { requestAnimationFrame(() => { if ($messagesContainer) $messagesContainer.scrollTop = $messagesContainer.scrollHeight; }); }
function showStop() { $sendBtn.style.display = 'none'; $stopBtn.style.display = 'flex'; }
function showSend() { $sendBtn.style.display = 'flex'; $stopBtn.style.display = 'none'; }
function showToolProgress(name, elapsed) { if (!$toolProgress) return; $toolProgress.style.display = 'flex'; if ($toolProgressText) $toolProgressText.textContent = `${name || 'Working'} ${elapsed ? elapsed.toFixed(1) + 's' : '...'}`; }
function hideToolProgress() { if ($toolProgress) $toolProgress.style.display = 'none'; }
function autoResize() { if (!$userInput) return; $userInput.style.height = 'auto'; $userInput.style.height = Math.min($userInput.scrollHeight, 200) + 'px'; }
function showPermissionBar(request) {
if (!$permissionBar) return;
$permissionBar.style.display = 'flex';
const text = $permissionBar.querySelector('.permission-text');
if (text) text.textContent = `${request?.tool_name || 'Tool'} wants to ${request?.input ? JSON.stringify(request.input).slice(0, 80) : 'execute'}`;
S.pendingPermission = request;
}
function hidePermissionBar() { if ($permissionBar) $permissionBar.style.display = 'none'; S.pendingPermission = null; }
// ============================================================
// Core
// ============================================================
function sendMessage() {
const content = $userInput?.value?.trim();
if (!content || S.isGenerating) return;
$userInput.value = '';
autoResize();
addUserMessage(content);
S.streamingText = '';
S.currentAssistantEl = createAssistantEl();
vscode.postMessage({ type: 'sendMessage', content, options: { includeFile: true } });
}
function stopGeneration() {
vscode.postMessage({ type: 'stopGeneration' });
S.isGenerating = false;
showSend();
hideToolProgress();
hidePermissionBar();
if (S.currentAssistantEl) { finalizeAssistant(S.currentAssistantEl, S.streamingText); S.currentAssistantEl = null; }
}
function newConversation() {
S.messages = [];
S.streamingText = '';
S.currentAssistantEl = null;
if ($messages) $messages.innerHTML = '';
showWelcome();
showSend();
hideToolProgress();
hidePermissionBar();
vscode.postMessage({ type: 'newConversation' });
vscode.postMessage({ type: 'generate_session_title' });
}
// ============================================================
// Handlers
// ============================================================
function handleMsg(msg) {
switch (msg.type) {
case 'init_response': S.settings = msg; break;
case 'generationStarted': S.isGenerating = true; showStop(); hideToolProgress(); break;
case 'systemInit':
if (msg.mcp_servers) { S.mcpServers = msg.mcp_servers; updateMcpBadge(); }
if (msg.model) { S.currentModel = msg.model; if ($modelLabel) $modelLabel.textContent = msg.model; }
break;
case 'streamToken': handleStreamToken(msg.event); break;
case 'assistantMessage': handleAssistantMsg(msg.message); break;
case 'toolProgress': showToolProgress(msg.tool_name || msg.toolName, msg.elapsed_time_seconds || msg.elapsed); break;
case 'tool_permission_request':
showPermissionBar(msg.request || msg);
vscode.postMessage({ type: 'control_response', request_id: msg.request_id || msg.requestId, response: { approved: true } });
break;
case 'result':
hideToolProgress();
hidePermissionBar();
if (msg.costUsd || msg.durationMs || msg.numTurns) addResultMessage(msg.costUsd, msg.durationMs, msg.numTurns);
if (msg.isError && msg.result) {
addErrorMessage(msg.result);
if (S.currentAssistantEl) { S.currentAssistantEl.remove(); S.currentAssistantEl = null; }
} else if (msg.result && !S.streamingText) {
S.streamingText = msg.result;
if (S.currentAssistantEl) finalizeAssistant(S.currentAssistantEl, S.streamingText);
else { const el = createAssistantEl(); finalizeAssistant(el, S.streamingText); }
} else if (S.currentAssistantEl) { finalizeAssistant(S.currentAssistantEl, S.streamingText); S.currentAssistantEl = null; }
break;
case 'generationComplete':
S.isGenerating = false; showSend(); hideToolProgress(); hidePermissionBar();
if (S.currentAssistantEl) { finalizeAssistant(S.currentAssistantEl, S.streamingText); S.currentAssistantEl = null; }
vscode.postMessage({ type: 'generate_session_title' });
break;
case 'error':
S.isGenerating = false; showSend(); hideToolProgress(); hidePermissionBar();
addErrorMessage(msg.content || msg.message || 'Unknown error');
if (S.currentAssistantEl) { S.currentAssistantEl.remove(); S.currentAssistantEl = null; }
break;
case 'rawText':
S.streamingText += msg.text;
if (S.currentAssistantEl) updateAssistant(S.currentAssistantEl, S.streamingText);
break;
case 'newConversation':
S.messages = []; S.streamingText = ''; S.currentAssistantEl = null;
if ($messages) $messages.innerHTML = '';
showWelcome(); showSend(); hideToolProgress(); hidePermissionBar();
S.sessionId = msg.sessionId;
break;
case 'focusInput': $userInput?.focus(); break;
case 'blurInput': $userInput?.blur(); break;
// Model
case 'get_auth_status_response':
if (msg.model) { S.currentModel = msg.model; if ($modelLabel) $modelLabel.textContent = msg.model; }
break;
case 'set_model_response':
S.currentModel = msg.model;
if ($modelLabel) $modelLabel.textContent = msg.model || 'Default';
break;
// Permission mode
case 'set_permission_mode_response':
S.permissionMode = msg.mode;
if ($modeBtn) $modeBtn.textContent = msg.mode === 'default' ? 'Default' : msg.mode === 'plan' ? 'Plan' : msg.mode === 'acceptEdits' ? 'Accept Edits' : msg.mode;
break;
// Sessions
case 'list_sessions_response':
showSessionsPanel(msg.sessions || []);
break;
case 'get_session_response':
if (msg.messages?.length) {
if ($messages) $messages.innerHTML = '';
showWelcome(); showSend();
for (const m of msg.messages) {
if (m.role === 'user') addUserMessage(m.content);
else if (m.role === 'assistant') {
const el = createAssistantEl();
finalizeAssistant(el, m.content);
}
}
S.messages = msg.messages;
S.sessionId = msg.sessionId;
}
break;
// Selection
case 'selection_changed':
if (msg.fileName && $contextBadge && $contextFilename) {
$contextFilename.textContent = msg.fileName.split('/').pop();
$contextBadge.style.display = 'flex';
S.activeFile = msg;
}
break;
case 'insertMention':
if ($userInput && msg.mention) { $userInput.value += msg.mention + ' '; $userInput.focus(); autoResize(); }
break;
case 'getSettings':
S.settings = msg.settings || {};
if (S.settings.model && $modelLabel) $modelLabel.textContent = S.settings.model;
if (S.settings.permissionMode && $modeBtn) $modeBtn.textContent = S.settings.permissionMode;
break;
case 'generate_session_title_response':
document.title = msg.title ? `Hermes - ${msg.title}` : 'Hermes Agent';
break;
case 'applyEdit':
break;
case 'get_mcp_servers_response':
S.mcpServers = msg.servers || [];
updateMcpBadge();
break;
case 'check_git_status_response':
break;
}
}
function handleStreamToken(event) {
if (!event) return;
if (event.type === 'content_block_delta') {
const d = event.delta;
if (d?.type === 'text_delta' && d.text) {
S.streamingText += d.text;
if (S.currentAssistantEl) updateAssistant(S.currentAssistantEl, S.streamingText);
}
} else if (event.type === 'content_block_start') {
if (event.content_block?.type === 'tool_use') addToolMessage(event.content_block.name, 'running');
} else if (event.type === 'content_block_stop') {
hideToolProgress();
}
}
function handleAssistantMsg(message) {
if (!message?.content) return;
for (const block of message.content) {
if (block.type === 'tool_use') addToolMessage(block.name, 'complete');
}
}
// ============================================================
// Sessions Panel (overlay)
// ============================================================
function showSessionsPanel(sessions) {
let panel = document.getElementById('sessions-panel');
if (panel) { panel.remove(); return; }
panel = document.createElement('div');
panel.id = 'sessions-panel';
panel.className = 'sessions-panel';
panel.innerHTML = `
<div class="sp-header">
<span>Past Conversations</span>
<button class="sp-close" onclick="document.getElementById('sessions-panel').remove()">&times;</button>
</div>
<div class="sp-list">
${sessions.length ? sessions.map(s => `
<div class="sp-item" data-id="${s.id}">
<div class="sp-title">${esc(s.title || 'Untitled')}</div>
<div class="sp-date">${s.createdAt ? new Date(s.createdAt).toLocaleDateString() : ''}</div>
<button class="sp-delete" data-id="${s.id}" title="Delete">&times;</button>
</div>
`).join('') : '<div class="sp-empty">No past conversations</div>'}
</div>`;
document.body.appendChild(panel);
panel.querySelectorAll('.sp-item').forEach(el => {
el.addEventListener('click', (e) => {
if (e.target.classList.contains('sp-delete')) {
vscode.postMessage({ type: 'delete_session', sessionId: e.target.dataset.id });
e.target.closest('.sp-item')?.remove();
return;
}
vscode.postMessage({ type: 'get_session', sessionId: el.dataset.id });
panel.remove();
});
});
}
// ============================================================
// MCP Server Management
// ============================================================
function updateMcpBadge() {
const connected = S.mcpServers.filter(s => s.status === 'connected').length;
const total = S.mcpServers.length;
const $badge = $('mcp-badge');
if ($badge) $badge.textContent = connected > 0 ? connected : '';
}
function showMcpPanel() {
let panel = document.getElementById('mcp-panel');
if (panel) { panel.remove(); return; }
panel = document.createElement('div');
panel.id = 'mcp-panel';
panel.className = 'mcp-panel';
panel.innerHTML = `
<div class="sp-header">
<span>MCP Servers</span>
<button class="sp-close" onclick="document.getElementById('mcp-panel').remove()">&times;</button>
</div>
<div class="sp-list">
${S.mcpServers.length ? S.mcpServers.map(s => `
<div class="mcp-item" data-name="${esc(s.name)}">
<div class="mcp-status-dot ${s.status === 'connected' ? 'mcp-on' : 'mcp-off'}"></div>
<div class="mcp-info">
<div class="mcp-name">${esc(s.name)}</div>
<div class="mcp-status-text">${s.status}</div>
</div>
<div class="mcp-actions">
${s.status === 'connected'
? `<button class="mcp-btn mcp-disable" data-name="${esc(s.name)}">Disable</button>`
: `<button class="mcp-btn mcp-enable" data-name="${esc(s.name)}">Enable</button>`}
<button class="mcp-btn mcp-reconnect" data-name="${esc(s.name)}">Reconnect</button>
</div>
</div>
`).join('') : '<div class="sp-empty">No MCP servers configured</div>'}
</div>`;
document.body.appendChild(panel);
panel.querySelectorAll('.mcp-enable').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
vscode.postMessage({ type: 'mcp_toggle', name: btn.dataset.name, enabled: true });
btn.closest('.mcp-item').querySelector('.mcp-status-dot').className = 'mcp-status-dot mcp-on';
btn.closest('.mcp-item').querySelector('.mcp-status-text').textContent = 'connected';
});
});
panel.querySelectorAll('.mcp-disable').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
vscode.postMessage({ type: 'mcp_toggle', name: btn.dataset.name, enabled: false });
btn.closest('.mcp-item').querySelector('.mcp-status-dot').className = 'mcp-status-dot mcp-off';
btn.closest('.mcp-item').querySelector('.mcp-status-text').textContent = 'disabled';
});
});
panel.querySelectorAll('.mcp-reconnect').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
vscode.postMessage({ type: 'reconnect_mcp_server', name: btn.dataset.name });
});
});
setTimeout(() => {
const closeHandler = (e) => { if (!panel.contains(e.target)) { panel.remove(); document.removeEventListener('click', closeHandler); } };
document.addEventListener('click', closeHandler);
}, 100);
}
// ============================================================
// Model Picker
// ============================================================
function showModelPicker() {
const existing = document.getElementById('model-picker');
if (existing) { existing.remove(); return; }
const models = [
{ label: 'Default', value: '' },
{ label: 'Claude Opus 4.6', value: 'anthropic/claude-opus-4-6' },
{ label: 'Claude Sonnet 4.6', value: 'anthropic/claude-sonnet-4-6' },
{ label: 'GPT-4.1', value: 'openai/gpt-4.1' },
{ label: 'Gemini 2.5 Pro', value: 'google/gemini-2.5-pro' },
{ label: 'Qwen 3 235B', value: 'qwen/qwen3-235b-a22b' },
];
const picker = document.createElement('div');
picker.id = 'model-picker';
picker.className = 'model-picker';
if ($modelBtn) {
const rect = $modelBtn.getBoundingClientRect();
picker.style.top = (rect.bottom + 4) + 'px';
picker.style.left = rect.left + 'px';
}
picker.innerHTML = `
<div class="mp-header">Select Model</div>
${models.map(m => `<div class="mp-item${m.value === S.currentModel ? ' active' : ''}" data-value="${m.value}">${m.label}</div>`).join('')}`;
document.body.appendChild(picker);
picker.querySelectorAll('.mp-item').forEach(el => {
el.addEventListener('click', (e) => {
e.stopPropagation();
S.currentModel = el.dataset.value;
if ($modelLabel) $modelLabel.textContent = el.textContent;
vscode.postMessage({ type: 'set_model', model: el.dataset.value });
picker.remove();
});
});
setTimeout(() => {
const closeHandler = (e) => { if (!picker.contains(e.target)) { picker.remove(); document.removeEventListener('click', closeHandler); } };
document.addEventListener('click', closeHandler);
}, 100);
}
// ============================================================
// Permission Mode Cycle
// ============================================================
const MODES = ['default', 'plan', 'acceptEdits', 'bypassPermissions'];
const MODE_LABELS = { default: 'Default', plan: 'Plan', acceptEdits: 'Accept Edits', bypassPermissions: 'Bypass' };
function cyclePermissionMode() {
const idx = MODES.indexOf(S.permissionMode);
const next = MODES[(idx + 1) % MODES.length];
S.permissionMode = next;
if ($modeBtn) $modeBtn.textContent = MODE_LABELS[next];
vscode.postMessage({ type: 'set_permission_mode', mode: next });
}
// ============================================================
// Events
// ============================================================
$sendBtn?.addEventListener('click', sendMessage);
$stopBtn?.addEventListener('click', stopGeneration);
$newChatBtn?.addEventListener('click', newConversation);
$newChatBtnTop?.addEventListener('click', newConversation);
$contextDismiss?.addEventListener('click', () => { if ($contextBadge) $contextBadge.style.display = 'none'; S.activeFile = null; });
$modelBtn?.addEventListener('click', (e) => { e.stopPropagation(); showModelPicker(); });
const $mcpBtn = $('mcp-btn');
$mcpBtn?.addEventListener('click', (e) => { e.stopPropagation(); showMcpPanel(); });
$sessionsBtn?.addEventListener('click', () => vscode.postMessage({ type: 'list_sessions' }));
$settingsBtn?.addEventListener('click', () => vscode.postMessage({ type: 'open_config' }));
$modeBtn?.addEventListener('click', cyclePermissionMode);
$permAllow?.addEventListener('click', () => {
if (S.pendingPermission) vscode.postMessage({ type: 'control_response', request_id: S.pendingPermission.request_id, response: { approved: true } });
hidePermissionBar();
});
$permDeny?.addEventListener('click', () => {
if (S.pendingPermission) vscode.postMessage({ type: 'control_response', request_id: S.pendingPermission.request_id, response: { approved: false } });
hidePermissionBar();
});
$userInput?.addEventListener('input', autoResize);
$userInput?.addEventListener('keydown', (e) => {
const useCtrlEnter = S.settings?.useCtrlEnterToSend;
if (e.key === 'Enter') {
if (useCtrlEnter) { if (e.metaKey || e.ctrlKey) { e.preventDefault(); sendMessage(); } }
else { if (!e.shiftKey && !e.metaKey && !e.ctrlKey) { e.preventDefault(); sendMessage(); } }
}
if (e.key === '@') {
vscode.postMessage({ type: 'insertAtMention' });
}
if (e.key === '/' && e.target.value === '') {
// Could show command palette
}
});
window.addEventListener('message', (event) => { if (event.data?.type) handleMsg(event.data); });
// Init
vscode.postMessage({ type: 'ready' });
vscode.postMessage({ type: 'getSettings' });
vscode.postMessage({ type: 'get_auth_status' });