Files
claude-code-chat/src/script.ts
andrepimenta 3d8bcf5241 Hide OpenCredits option when feature flag is disabled and bump to 2.0.5
Only show "Just try it · Pay as you go with OpenCredits" in the
install modal when the OpenCredits feature flag is enabled for
the user's region.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 21:21:11 +01:00

5250 lines
191 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import getSkillsScript from './skills-script';
import getPluginsScript from './plugins-script';
const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https://ccc.api.opencredits.ai', opencreditsWebUrl: string = 'https://ccc.opencredits.ai', opencreditsPublishableKey: string = 'oc_pk_c43da4f9a9484ae484ad29bc97cc354f') => `<script>
var OPENCREDITS_API_URL = '${opencreditsApiUrl}';
var OPENCREDITS_WEB_URL = '${opencreditsWebUrl}';
// ─── OpenCredits SDK (inlined) ───
!function(){var e="https://opencredits.ai",t="",o="",n={},a=null,r=null,i=null,d=null,c=!1,s=null;function l(){var e=document.getElementById("oc-overlay");e&&(e.remove(),document.body.style.overflow=""),r=null,i=null}function u(e){if(e){a=e;try{localStorage.setItem(d,e)}catch(e){}}}function p(e,t){n[e]&&n[e](function(e){if(!e||"object"!=typeof e)return e;var t={};for(var o in e)"source"!==o&&"event"!==o&&(t[o]=e[o]);return t}(t))}function m(e){if(e.data&&"opencredits"===e.data.source&&r&&e.source===r.contentWindow&&e.origin===o){var t=e.data.event;if("ready"===t){var l={source:"opencredits_init",publishable_key:d,currency:n.currency||"usd",checkout_mode:n.checkoutMode||"embedded"};i&&i.amount&&(l.amount=i.amount),i&&i.model&&(l.model=i.model),n.inputTokens&&(l.input_tokens=n.inputTokens),n.outputTokens&&(l.output_tokens=n.outputTokens),a&&(l.user_key=a),r.contentWindow.postMessage(l,o)}else if("checkout_opened"===t)u(e.data.user_key),e.data.checkout_url&&"new_tab"===n.checkoutMode&&window.open(e.data.checkout_url,"_blank"),p("onCheckoutOpened",e.data);else if("purchase_completed"===t)u(e.data.user_key),c=!0,s={user_key:a,balance:e.data.balance,credits_added:e.data.credits_added},p("onPurchaseCompleted",e.data),e.data.can_close&&p("onComplete",s);else if("purchase_error"===t)p("onPurchaseError",e.data),p("onError",e.data);else if("login_completed"===t)u(e.data.user_key),p("onLoginCompleted",e.data);else if("logged_out"===t){a=null;try{localStorage.removeItem(d)}catch(e){}p("onLoggedOut",e.data)}else"copy_url"===t?p("onCopyUrl",e.data):"open_url"===t?p("onOpenUrl",e.data):"account_details_shown"===t?p("onAccountDetailsShown",e.data):"account_details_dismissed"===t&&(p("onAccountDetailsDismissed",e.data),c&&p("onComplete",s))}}window.OpenCredits={init:function(r){if(!(d=(n=r||{}).publishableKey))throw new Error("OpenCredits: publishableKey is required");e=n.baseUrl||window.OPENCREDITS_ORIGIN||"https://opencredits.ai",t=e+"/embed/checkout.html",o=new URL(t).origin,a=localStorage.getItem(d)||n.userKey||null,window.addEventListener("message",m)},open:function(e){if(e=e||{},!d)throw new Error("OpenCredits: call init() first");c=!1,s=null;var o=document.createElement("div");o.id="oc-overlay",o.style.cssText="position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;animation:ocFadeIn .2s ease";var n=document.createElement("div");n.style.cssText="background:#fff;border-radius:16px;width:480px;max-width:95vw;height:680px;max-height:90vh;overflow:hidden;position:relative;box-shadow:0 24px 48px rgba(0,0,0,0.2);animation:ocSlideUp .25s ease";var a=document.createElement("button");if(a.innerHTML="&times;",a.style.cssText="position:absolute;top:12px;right:12px;z-index:10;width:32px;height:32px;border-radius:50%;border:none;background:rgba(0,0,0,0.06);cursor:pointer;font-size:18px;color:#666;display:flex;align-items:center;justify-content:center",a.onmouseover=function(){a.style.background="rgba(0,0,0,0.12)",a.style.color="#000"},a.onmouseout=function(){a.style.background="rgba(0,0,0,0.06)",a.style.color="#666"},a.onclick=function(){l()},(r=document.createElement("iframe")).style.cssText="width:100%;height:100%;border:none",r.src=t,n.appendChild(a),n.appendChild(r),o.appendChild(n),o.onclick=function(e){e.target===o&&l()},document.body.appendChild(o),document.body.style.overflow="hidden",i=e,!document.getElementById("oc-keyframes")){var u=document.createElement("style");u.id="oc-keyframes",u.textContent="@keyframes ocFadeIn{from{opacity:0}to{opacity:1}}@keyframes ocSlideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}",document.head.appendChild(u)}},close:l,getUserKey:function(){return a}}}();
// ─── Initialize OpenCredits SDK ───
OpenCredits.init({
publishableKey: '${opencreditsPublishableKey}',
baseUrl: OPENCREDITS_WEB_URL,
checkoutMode: 'manual',
currency: 'usd',
inputTokens: 25000,
outputTokens: 25000,
onCheckoutOpened: function(data) {
// Save user key early so it persists even if VS Code closes during payment
if (data.user_key) {
vscode.postMessage({
type: 'saveOpenCreditsKeyEarly',
key: data.user_key
});
}
if (data.checkout_url) {
vscode.postMessage({
type: 'openExternalUrl',
url: data.checkout_url
});
}
},
onPurchaseCompleted: function(data) {
sendStats('Checkout completed');
vscode.postMessage({
type: 'opencreditsKeyFromCheckout',
key: data.user_key
});
},
onLoginCompleted: function(data) {
vscode.postMessage({
type: 'opencreditsKeyFromCheckout',
key: data.user_key
});
},
onCopyUrl: function(data) {
if (data.url) {
vscode.postMessage({ type: 'copyToClipboard', text: data.url });
}
},
onOpenUrl: function(data) {
if (data.url) {
vscode.postMessage({ type: 'openExternalUrl', url: data.url });
}
},
onPurchaseError: function(data) {
console.error('OpenCredits purchase error:', data);
},
onComplete: function() {
OpenCredits.close();
}
});
const vscode = acquireVsCodeApi();
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const statusDiv = document.getElementById('status');
const statusTextDiv = document.getElementById('statusText');
const filePickerModal = document.getElementById('filePickerModal');
const fileSearchInput = document.getElementById('fileSearchInput');
const fileList = document.getElementById('fileList');
const imageBtn = document.getElementById('imageBtn');
let isProcessRunning = false;
let filteredFiles = [];
let selectedFileIndex = -1;
let planModeEnabled = false;
let thinkingModeEnabled = false;
let lastPendingEditIndex = -1; // Track the last Edit/MultiEdit/Write toolUse without result
let lastPendingEditData = null; // Store diff data for the pending edit { filePath, oldContent, newContent }
let attachedImages = []; // Array of { filePath, previewUri }
// Open diff using stored data (no file read needed)
function openDiffEditor() {
if (lastPendingEditData) {
vscode.postMessage({
type: 'openDiff',
filePath: lastPendingEditData.filePath,
oldContent: lastPendingEditData.oldContent,
newContent: lastPendingEditData.newContent
});
}
}
function shouldAutoScroll(messagesDiv) {
const threshold = 100; // pixels from bottom
const scrollTop = messagesDiv.scrollTop;
const scrollHeight = messagesDiv.scrollHeight;
const clientHeight = messagesDiv.clientHeight;
return (scrollTop + clientHeight >= scrollHeight - threshold);
}
function scrollToBottomIfNeeded(messagesDiv, shouldScroll = null) {
// If shouldScroll is not provided, check current scroll position
if (shouldScroll === null) {
shouldScroll = shouldAutoScroll(messagesDiv);
}
if (shouldScroll) {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
}
function addMessage(content, type = 'claude') {
const messagesDiv = document.getElementById('messages');
const shouldScroll = shouldAutoScroll(messagesDiv);
const messageDiv = document.createElement('div');
messageDiv.className = \`message \${type}\`;
// Add header for main message types (excluding system)
if (type === 'user' || type === 'claude' || type === 'error') {
const headerDiv = document.createElement('div');
headerDiv.className = 'message-header';
const iconDiv = document.createElement('div');
iconDiv.className = \`message-icon \${type}\`;
const labelDiv = document.createElement('div');
labelDiv.className = 'message-label';
// Set icon and label based on type
switch(type) {
case 'user':
iconDiv.textContent = '👤';
labelDiv.textContent = 'You';
break;
case 'claude':
iconDiv.textContent = '🤖';
labelDiv.textContent = 'Claude';
break;
case 'error':
iconDiv.textContent = '⚠️';
labelDiv.textContent = 'Error';
break;
}
// Add copy button
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-btn';
copyBtn.title = 'Copy message';
copyBtn.onclick = () => copyMessageContent(messageDiv);
copyBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>';
headerDiv.appendChild(iconDiv);
headerDiv.appendChild(labelDiv);
headerDiv.appendChild(copyBtn);
messageDiv.appendChild(headerDiv);
}
// Add content
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
if(type == 'user' || type === 'claude' || type === 'thinking'){
contentDiv.innerHTML = content;
} else {
const preElement = document.createElement('pre');
preElement.textContent = content;
contentDiv.appendChild(preElement);
}
messageDiv.appendChild(contentDiv);
// Check if this is a permission-related error and add yolo mode button
if (type === 'error' && isPermissionError(content)) {
const yoloSuggestion = document.createElement('div');
yoloSuggestion.className = 'yolo-suggestion';
yoloSuggestion.innerHTML = \`
<div class="yolo-suggestion-text">
<span>💡 This looks like a permission issue. You can enable Yolo Mode to skip all permission checks.</span>
</div>
<button class="yolo-suggestion-btn" onclick="enableYoloMode()">Enable Yolo Mode</button>
\`;
messageDiv.appendChild(yoloSuggestion);
}
messagesDiv.appendChild(messageDiv);
moveProcessingIndicatorToLast();
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
}
function addToolUseMessage(data) {
const messagesDiv = document.getElementById('messages');
const shouldScroll = shouldAutoScroll(messagesDiv);
const messageDiv = document.createElement('div');
messageDiv.className = 'message tool';
// Create modern header with icon
const headerDiv = document.createElement('div');
headerDiv.className = 'tool-header';
const iconDiv = document.createElement('div');
iconDiv.className = 'tool-icon';
iconDiv.textContent = data.toolName === 'ExitPlanMode' ? '📋' : '🔧';
const toolInfoElement = document.createElement('div');
toolInfoElement.className = 'tool-info';
let toolName = data.toolInfo.replace('🔧 Executing: ', '');
if (toolName === 'TodoWrite') {
toolName = 'Update Todos';
} else if (toolName === 'ExitPlanMode') {
toolName = 'Plan';
}
toolInfoElement.textContent = toolName;
headerDiv.appendChild(iconDiv);
headerDiv.appendChild(toolInfoElement);
messageDiv.appendChild(headerDiv);
if (data.rawInput) {
const inputElement = document.createElement('div');
inputElement.className = 'tool-input';
const contentDiv = document.createElement('div');
contentDiv.className = 'tool-input-content';
// Handle TodoWrite specially or format raw input
if (data.toolName === 'TodoWrite' && data.rawInput.todos) {
let todoHtml = 'Todo List Update:';
for (const todo of data.rawInput.todos) {
const status = todo.status === 'completed' ? '✅' :
todo.status === 'in_progress' ? '🔄' : '⏳';
todoHtml += '\\n' + status + ' ' + todo.content;
}
contentDiv.innerHTML = todoHtml;
} else {
// Format raw input with expandable content for long values
// Use diff format for Edit, MultiEdit, and Write tools, regular format for others
if (data.toolName === 'Edit' || data.toolName === 'MultiEdit' || data.toolName === 'Write') {
// Only show Open Diff button if we have fileContentBefore (live session, not reload)
const showButton = data.fileContentBefore !== undefined && data.messageIndex >= 0;
// Hide any existing pending edit button before showing new one
if (showButton && lastPendingEditIndex >= 0) {
const prevContent = document.querySelector('[data-edit-message-index="' + lastPendingEditIndex + '"]');
if (prevContent) {
const btn = prevContent.querySelector('.diff-open-btn');
if (btn) btn.style.display = 'none';
}
lastPendingEditData = null;
}
if (showButton) {
lastPendingEditIndex = data.messageIndex;
contentDiv.setAttribute('data-edit-message-index', data.messageIndex);
// Compute and store diff data for when button is clicked
const oldContent = data.fileContentBefore || '';
let newContent = oldContent;
if (data.toolName === 'Edit' && data.rawInput.old_string && data.rawInput.new_string) {
newContent = oldContent.replace(data.rawInput.old_string, data.rawInput.new_string);
} else if (data.toolName === 'MultiEdit' && data.rawInput.edits) {
for (const edit of data.rawInput.edits) {
if (edit.old_string && edit.new_string) {
newContent = newContent.replace(edit.old_string, edit.new_string);
}
}
} else if (data.toolName === 'Write' && data.rawInput.content) {
newContent = data.rawInput.content;
}
lastPendingEditData = {
filePath: data.rawInput.file_path,
oldContent: oldContent,
newContent: newContent
};
}
if (data.toolName === 'Edit') {
contentDiv.innerHTML = formatEditToolDiff(data.rawInput, data.fileContentBefore, showButton, data.startLine);
} else if (data.toolName === 'MultiEdit') {
contentDiv.innerHTML = formatMultiEditToolDiff(data.rawInput, data.fileContentBefore, showButton, data.startLines);
} else {
contentDiv.innerHTML = formatWriteToolDiff(data.rawInput, data.fileContentBefore, showButton);
}
} else if (data.toolName === 'ExitPlanMode' && data.rawInput) {
contentDiv.innerHTML = formatPlanOutput(data.rawInput);
} else {
contentDiv.innerHTML = formatToolInputUI(data.rawInput);
}
}
inputElement.appendChild(contentDiv);
messageDiv.appendChild(inputElement);
} else if (data.toolInput) {
// Fallback for pre-formatted input
const inputElement = document.createElement('div');
inputElement.className = 'tool-input';
const labelDiv = document.createElement('div');
labelDiv.className = 'tool-input-label';
labelDiv.textContent = 'INPUT';
inputElement.appendChild(labelDiv);
const contentDiv = document.createElement('div');
contentDiv.className = 'tool-input-content';
contentDiv.textContent = data.toolInput;
inputElement.appendChild(contentDiv);
messageDiv.appendChild(inputElement);
}
messagesDiv.appendChild(messageDiv);
moveProcessingIndicatorToLast();
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
}
function createExpandableInput(toolInput, rawInput) {
try {
let html = toolInput.replace(/\\[expand\\]/g, '<span class="expand-btn" onclick="toggleExpand(this)">expand</span>');
// Store raw input data for expansion
if (rawInput && typeof rawInput === 'object') {
let btnIndex = 0;
html = html.replace(/<span class="expand-btn"[^>]*>expand<\\/span>/g, (match) => {
const keys = Object.keys(rawInput);
const key = keys[btnIndex] || '';
const value = rawInput[key] || '';
const valueStr = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
const escapedValue = valueStr.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
btnIndex++;
return \`<span class="expand-btn" data-key="\${key}" data-value="\${escapedValue}" onclick="toggleExpand(this)">expand</span>\`;
});
}
return html;
} catch (error) {
console.error('Error creating expandable input:', error);
return toolInput;
}
}
function addToolResultMessage(data) {
const messagesDiv = document.getElementById('messages');
const shouldScroll = shouldAutoScroll(messagesDiv);
// When result comes in for Edit/MultiEdit/Write, hide the Open Diff button on the request
// since the edit has now been applied (no longer pending)
if (lastPendingEditIndex >= 0) {
// Find and hide the button on the corresponding toolUse
const toolUseContent = document.querySelector('[data-edit-message-index="' + lastPendingEditIndex + '"]');
if (toolUseContent) {
const btn = toolUseContent.querySelector('.diff-open-btn');
if (btn) {
btn.style.display = 'none';
}
}
lastPendingEditIndex = -1;
lastPendingEditData = null;
}
// For Read and TodoWrite tools, just hide loading state (no result message needed)
if ((data.toolName === 'Read' || data.toolName === 'TodoWrite') && !data.isError) {
return;
}
// For Edit/MultiEdit/Write, show simple completion message (diff is already shown on request)
if ((data.toolName === 'Edit' || data.toolName === 'MultiEdit' || data.toolName === 'Write') && !data.isError) {
let completionText;
if (data.toolName === 'Edit') {
completionText = '✅ Edit completed';
} else if (data.toolName === 'MultiEdit') {
completionText = '✅ MultiEdit completed';
} else {
completionText = '✅ Write completed';
}
addMessage(completionText, 'system');
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
return;
}
if(data.isError && data.content?.includes("File has not been read yet. Read it first before writing to it.")){
return addMessage("File has not been read yet. Let me read it first before writing to it.", 'system');
}
const messageDiv = document.createElement('div');
messageDiv.className = data.isError ? 'message error' : 'message tool-result';
// Create header
const headerDiv = document.createElement('div');
headerDiv.className = 'message-header';
const iconDiv = document.createElement('div');
iconDiv.className = data.isError ? 'message-icon error' : 'message-icon';
iconDiv.style.background = data.isError ?
'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)' :
'linear-gradient(135deg, #1cc08c 0%, #16a974 100%)';
iconDiv.textContent = data.isError ? '❌' : '✅';
const labelDiv = document.createElement('div');
labelDiv.className = 'message-label';
labelDiv.textContent = data.isError ? 'Error' : 'Result';
headerDiv.appendChild(iconDiv);
headerDiv.appendChild(labelDiv);
messageDiv.appendChild(headerDiv);
// Add content
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
// Check if it's a tool result and truncate appropriately
let content = data.content;
// Clean up error messages by removing XML-like tags
if (data.isError && content) {
content = content.replace(/<tool_use_error>/g, '').replace(/<\\/tool_use_error>/g, '').trim();
}
if (content.length > 200 && !data.isError) {
const truncateAt = 197;
const truncated = content.substring(0, truncateAt);
const resultId = 'result_' + Math.random().toString(36).substr(2, 9);
const preElement = document.createElement('pre');
preElement.innerHTML = '<span id="' + resultId + '_visible">' + escapeHtml(truncated) + '</span>' +
'<span id="' + resultId + '_ellipsis">...</span>' +
'<span id="' + resultId + '_hidden" style="display: none;">' + escapeHtml(content.substring(truncateAt)) + '</span>';
contentDiv.appendChild(preElement);
// Add expand button container
const expandContainer = document.createElement('div');
expandContainer.className = 'diff-expand-container';
const expandButton = document.createElement('button');
expandButton.className = 'diff-expand-btn';
expandButton.textContent = 'Show more';
expandButton.setAttribute('onclick', 'toggleResultExpansion(\\'' + resultId + '\\\')');
expandContainer.appendChild(expandButton);
contentDiv.appendChild(expandContainer);
} else {
const preElement = document.createElement('pre');
preElement.textContent = content;
contentDiv.appendChild(preElement);
}
messageDiv.appendChild(contentDiv);
// Check if this is a permission-related error and add yolo mode button
if (data.isError && isPermissionError(content)) {
const yoloSuggestion = document.createElement('div');
yoloSuggestion.className = 'yolo-suggestion';
yoloSuggestion.innerHTML = \`
<div class="yolo-suggestion-text">
<span>💡 This looks like a permission issue. You can enable Yolo Mode to skip all permission checks.</span>
</div>
<button class="yolo-suggestion-btn" onclick="enableYoloMode()">Enable Yolo Mode</button>
\`;
messageDiv.appendChild(yoloSuggestion);
}
messagesDiv.appendChild(messageDiv);
moveProcessingIndicatorToLast();
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
}
function formatPlanOutput(input) {
var html = '';
// Render plan markdown
if (input.plan) {
html += '<div class="plan-content">' + parseSimpleMarkdown(input.plan) + '</div>';
}
// Render allowed prompts as action buttons
if (input.allowedPrompts && input.allowedPrompts.length > 0) {
html += '<div class="plan-actions">';
html += '<div class="plan-actions-label">Suggested actions:</div>';
input.allowedPrompts.forEach(function(p) {
var label = p.prompt || (p.tool + ' command');
var escapedPrompt = escapeHtml(label).replace(/'/g, '&#39;');
html += '<button class="plan-action-btn" onclick="sendPlanAction(&#39;' + escapedPrompt + '&#39;)" title="' + escapeHtml(p.tool) + '">' + escapeHtml(label) + '</button>';
});
html += '</div>';
}
return html;
}
function sendPlanAction(prompt) {
messageInput.value = prompt;
sendMessage();
}
function formatToolInputUI(input) {
if (!input || typeof input !== 'object') {
const str = String(input);
if (str.length > 100) {
const truncateAt = 97;
const truncated = str.substring(0, truncateAt);
const inputId = 'input_' + Math.random().toString(36).substr(2, 9);
return '<span id="' + inputId + '_visible">' + escapeHtml(truncated) + '</span>' +
'<span id="' + inputId + '_ellipsis">...</span>' +
'<span id="' + inputId + '_hidden" style="display: none;">' + escapeHtml(str.substring(truncateAt)) + '</span>' +
'<div class="diff-expand-container">' +
'<button class="diff-expand-btn" onclick="toggleResultExpansion(\\\'' + inputId + '\\\')">Show more</button>' +
'</div>';
}
return str;
}
// Special handling for Read tool with file_path
if (input.file_path && Object.keys(input).length === 1) {
const formattedPath = formatFilePath(input.file_path);
return '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(input.file_path) + '\\\')">' + formattedPath + '</div>';
}
let result = '';
let isFirst = true;
for (const [key, value] of Object.entries(input)) {
const valueStr = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
if (!isFirst) result += '\\n';
isFirst = false;
// Special formatting for file_path in Read tool context
if (key === 'file_path') {
const formattedPath = formatFilePath(valueStr);
result += '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(valueStr) + '\\\')">' + formattedPath + '</div>';
} else if (valueStr.length > 100) {
const truncated = valueStr.substring(0, 97) + '...';
const escapedValue = valueStr.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
result += '<span class="expandable-item"><strong>' + key + ':</strong> ' + truncated + ' <span class="expand-btn" data-key="' + key + '" data-value="' + escapedValue + '" onclick="toggleExpand(this)">expand</span></span>';
} else {
result += '<strong>' + key + ':</strong> ' + valueStr;
}
}
return result;
}
// Simple LCS-based diff algorithm
function computeLineDiff(oldLines, newLines) {
// Compute longest common subsequence
const m = oldLines.length;
const n = newLines.length;
const lcs = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (oldLines[i - 1] === newLines[j - 1]) {
lcs[i][j] = lcs[i - 1][j - 1] + 1;
} else {
lcs[i][j] = Math.max(lcs[i - 1][j], lcs[i][j - 1]);
}
}
}
// Backtrack to build diff
const diff = [];
let i = m, j = n;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
diff.unshift({type: 'context', oldLine: i - 1, newLine: j - 1, content: oldLines[i - 1]});
i--;
j--;
} else if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
diff.unshift({type: 'added', newLine: j - 1, content: newLines[j - 1]});
j--;
} else if (i > 0) {
diff.unshift({type: 'removed', oldLine: i - 1, content: oldLines[i - 1]});
i--;
}
}
return diff;
}
// Parse tool result to extract line numbers
function parseToolResult(resultContent) {
if (!resultContent || typeof resultContent !== 'string') {
return {startLine: 1, lines: []};
}
const lines = resultContent.split('\\n');
const parsed = [];
let startLine = null;
for (const line of lines) {
const match = line.match(/^\\s*(\\d+)→(.*)$/);
if (match) {
const lineNum = parseInt(match[1]);
const content = match[2];
if (startLine === null) startLine = lineNum;
parsed.push({num: lineNum, content});
}
}
return {startLine: startLine || 1, lines: parsed};
}
// Generate unified diff HTML with line numbers
// showButton controls whether to show the "Open Diff" button
function generateUnifiedDiffHTML(oldString, newString, filePath, startLine = 1, showButton = false) {
const oldLines = oldString.split('\\n');
const newLines = newString.split('\\n');
const diff = computeLineDiff(oldLines, newLines);
// Generate unique ID for this diff (used for truncation)
const diffId = 'diff_' + Math.random().toString(36).substr(2, 9);
let html = '';
const formattedPath = formatFilePath(filePath);
// Header with file path
html += '<div class="diff-file-header">';
html += '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(filePath) + '\\\')">' + formattedPath + '</div>';
html += '</div>\\n';
// Calculate line range
let firstLine = startLine;
let lastLine = startLine;
let addedCount = 0;
let removedCount = 0;
// Calculate actual line numbers
for (const change of diff) {
if (change.type === 'added') addedCount++;
if (change.type === 'removed') removedCount++;
}
lastLine = startLine + newLines.length - 1;
html += '<div class="diff-container">';
html += '<div class="diff-header">Lines ' + firstLine + '-' + lastLine + '</div>';
// Build diff lines with proper line numbers
let oldLineNum = startLine;
let newLineNum = startLine;
const maxLines = 6;
let lineIndex = 0;
// First pass: build all line HTML
const allLinesHtml = [];
for (const change of diff) {
let lineNum, prefix, cssClass;
if (change.type === 'context') {
lineNum = newLineNum;
prefix = ' ';
cssClass = 'context';
oldLineNum++;
newLineNum++;
} else if (change.type === 'added') {
lineNum = newLineNum;
prefix = '+';
cssClass = 'added';
newLineNum++;
} else {
lineNum = oldLineNum;
prefix = '-';
cssClass = 'removed';
oldLineNum++;
}
const lineNumStr = lineNum.toString().padStart(3);
allLinesHtml.push('<div class="diff-line ' + cssClass + '">' + prefix + lineNumStr + ' ' + escapeHtml(change.content) + '</div>');
}
// Show visible lines
const shouldTruncate = allLinesHtml.length > maxLines;
const visibleLines = shouldTruncate ? allLinesHtml.slice(0, maxLines) : allLinesHtml;
const hiddenLines = shouldTruncate ? allLinesHtml.slice(maxLines) : [];
html += '<div id="' + diffId + '_visible">';
html += visibleLines.join('');
html += '</div>';
// Show hidden lines (initially hidden)
if (shouldTruncate) {
html += '<div id="' + diffId + '_hidden" style="display: none;">';
html += hiddenLines.join('');
html += '</div>';
// Add expand button
html += '<div class="diff-expand-container">';
html += '<button class="diff-expand-btn" onclick="toggleDiffExpansion(\\'' + diffId + '\\')">Show ' + hiddenLines.length + ' more lines</button>';
html += '</div>';
}
html += '</div>';
// Summary
let summary = '';
if (addedCount > 0 && removedCount > 0) {
summary = '+' + addedCount + ' line' + (addedCount > 1 ? 's' : '') + ' added, -' + removedCount + ' line' + (removedCount > 1 ? 's' : '') + ' removed';
} else if (addedCount > 0) {
summary = '+' + addedCount + ' line' + (addedCount > 1 ? 's' : '') + ' added';
} else if (removedCount > 0) {
summary = '-' + removedCount + ' line' + (removedCount > 1 ? 's' : '') + ' removed';
}
if (summary) {
html += '<div class="diff-summary-row">';
html += '<span class="diff-summary">Summary: ' + summary + '</span>';
if (showButton) {
html += '<button class="diff-open-btn" onclick="openDiffEditor()" title="Open side-by-side diff in VS Code">';
html += '<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><rect x="9" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><line x1="2.5" y1="4" x2="5.5" y2="4" stroke="#e8a0a0" stroke-width="1.5"/><line x1="2.5" y1="7" x2="5.5" y2="7" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="2.5" y1="10" x2="5.5" y2="10" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="4" x2="13.5" y2="4" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="7" x2="13.5" y2="7" stroke="#8fd48f" stroke-width="1.5"/><line x1="10.5" y1="10" x2="13.5" y2="10" stroke="#8fd48f" stroke-width="1.5"/></svg>';
html += 'Open Diff</button>';
}
html += '</div>';
}
return html;
}
function formatEditToolDiff(input, fileContentBefore, showButton = false, providedStartLine = null) {
if (!input || typeof input !== 'object') {
return formatToolInputUI(input);
}
// Check if this is an Edit tool (has file_path, old_string, new_string)
if (!input.file_path || !input.old_string || !input.new_string) {
return formatToolInputUI(input);
}
// Use provided startLine if available (from saved data), otherwise compute from fileContentBefore
let startLine = providedStartLine || 1;
if (!providedStartLine && fileContentBefore) {
const position = fileContentBefore.indexOf(input.old_string);
if (position !== -1) {
// Count newlines before the match to get line number
const textBefore = fileContentBefore.substring(0, position);
startLine = (textBefore.match(/\\n/g) || []).length + 1;
}
}
return generateUnifiedDiffHTML(input.old_string, input.new_string, input.file_path, startLine, showButton);
}
function formatMultiEditToolDiff(input, fileContentBefore, showButton = false, providedStartLines = null) {
if (!input || typeof input !== 'object') {
return formatToolInputUI(input);
}
// Check if this is a MultiEdit tool (has file_path and edits array)
if (!input.file_path || !input.edits || !Array.isArray(input.edits)) {
return formatToolInputUI(input);
}
// Show full diffs for each edit
const formattedPath = formatFilePath(input.file_path);
let html = '<div class="diff-file-header">';
html += '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(input.file_path) + '\\\')">' + formattedPath + '</div>';
html += '</div>\\n';
input.edits.forEach((edit, index) => {
if (edit.old_string && edit.new_string) {
if (index > 0) {
html += '<div class="diff-edit-separator"></div>';
}
// Use provided startLine if available, otherwise compute from fileContentBefore
let startLine = (providedStartLines && providedStartLines[index]) || 1;
if (!providedStartLines && fileContentBefore) {
const position = fileContentBefore.indexOf(edit.old_string);
if (position !== -1) {
const textBefore = fileContentBefore.substring(0, position);
startLine = (textBefore.match(/\\n/g) || []).length + 1;
}
}
const oldLines = edit.old_string.split('\\n');
const newLines = edit.new_string.split('\\n');
const diff = computeLineDiff(oldLines, newLines);
html += '<div class="diff-container">';
html += '<div class="diff-header">Edit ' + (index + 1) + ' (Line ' + startLine + ')</div>';
let lineNum = startLine;
for (const change of diff) {
let prefix, cssClass;
if (change.type === 'context') {
prefix = ' ';
cssClass = 'context';
lineNum++;
} else if (change.type === 'added') {
prefix = '+';
cssClass = 'added';
lineNum++;
} else {
prefix = '-';
cssClass = 'removed';
}
const lineNumStr = (change.type === 'removed' ? '' : lineNum - 1).toString().padStart(3);
html += '<div class="diff-line ' + cssClass + '">' + prefix + lineNumStr + ' ' + escapeHtml(change.content) + '</div>';
}
html += '</div>';
}
});
// Add summary row with Open Diff button
html += '<div class="diff-summary-row">';
html += '<span class="diff-summary">Summary: ' + input.edits.length + ' edit' + (input.edits.length > 1 ? 's' : '') + '</span>';
if (showButton) {
html += '<button class="diff-open-btn" onclick="openDiffEditor()" title="Open side-by-side diff in VS Code">';
html += '<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><rect x="9" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><line x1="2.5" y1="4" x2="5.5" y2="4" stroke="#e8a0a0" stroke-width="1.5"/><line x1="2.5" y1="7" x2="5.5" y2="7" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="2.5" y1="10" x2="5.5" y2="10" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="4" x2="13.5" y2="4" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="7" x2="13.5" y2="7" stroke="#8fd48f" stroke-width="1.5"/><line x1="10.5" y1="10" x2="13.5" y2="10" stroke="#8fd48f" stroke-width="1.5"/></svg>';
html += 'Open Diff</button>';
}
html += '</div>';
return html;
}
function formatWriteToolDiff(input, fileContentBefore, showButton = false) {
if (!input || typeof input !== 'object') {
return formatToolInputUI(input);
}
// Check if this is a Write tool (has file_path and content)
if (!input.file_path || !input.content) {
return formatToolInputUI(input);
}
// fileContentBefore may be empty string if new file, or existing content if overwriting
const fullFileBefore = fileContentBefore || '';
// Show full content as added lines (new file or replacement)
return generateUnifiedDiffHTML(fullFileBefore, input.content, input.file_path, 1, showButton);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function openFileInEditor(filePath) {
vscode.postMessage({
type: 'openFile',
filePath: filePath
});
}
function formatFilePath(filePath) {
if (!filePath) return '';
// Extract just the filename
const parts = filePath.split('/');
const fileName = parts[parts.length - 1];
return '<span class="file-path-truncated" title="' + escapeHtml(filePath) + '" data-file-path="' + escapeHtml(filePath) + '">' +
'<span class="file-icon">📄</span>' + escapeHtml(fileName) + '</span>';
}
function toggleDiffExpansion(diffId) {
const hiddenDiv = document.getElementById(diffId + '_hidden');
const button = document.querySelector('[onclick*="' + diffId + '"]');
if (hiddenDiv && button) {
if (hiddenDiv.style.display === 'none') {
hiddenDiv.style.display = 'block';
button.textContent = 'Show less';
} else {
hiddenDiv.style.display = 'none';
const hiddenLines = hiddenDiv.querySelectorAll('.diff-line').length;
button.textContent = 'Show ' + hiddenLines + ' more lines';
}
}
}
function toggleResultExpansion(resultId) {
const hiddenDiv = document.getElementById(resultId + '_hidden');
const ellipsis = document.getElementById(resultId + '_ellipsis');
const button = document.querySelector('[onclick*="toggleResultExpansion(\\'' + resultId + '\\\')"]');
if (hiddenDiv && button) {
if (hiddenDiv.style.display === 'none') {
hiddenDiv.style.display = 'inline';
if (ellipsis) ellipsis.style.display = 'none';
button.textContent = 'Show less';
} else {
hiddenDiv.style.display = 'none';
if (ellipsis) ellipsis.style.display = 'inline';
button.textContent = 'Show more';
}
}
}
function toggleExpand(button) {
const key = button.getAttribute('data-key');
const value = button.getAttribute('data-value');
// Find the container that holds just this key-value pair
let container = button.parentNode;
while (container && !container.classList.contains('expandable-item')) {
container = container.parentNode;
}
if (!container) {
// Fallback: create a wrapper around the current line
const parent = button.parentNode;
const wrapper = document.createElement('div');
wrapper.className = 'expandable-item';
parent.insertBefore(wrapper, button.previousSibling || button);
// Move the key, value text, and button into the wrapper
let currentNode = wrapper.nextSibling;
const nodesToMove = [];
while (currentNode && currentNode !== button.nextSibling) {
nodesToMove.push(currentNode);
currentNode = currentNode.nextSibling;
}
nodesToMove.forEach(node => wrapper.appendChild(node));
container = wrapper;
}
if (button.textContent === 'expand') {
// Show full content
const decodedValue = value.replace(/&quot;/g, '"').replace(/&#39;/g, "'");
container.innerHTML = '<strong>' + key + ':</strong> ' + decodedValue + ' <span class="expand-btn" data-key="' + key + '" data-value="' + value + '" onclick="toggleExpand(this)">collapse</span>';
} else {
// Show truncated content
const decodedValue = value.replace(/&quot;/g, '"').replace(/&#39;/g, "'");
const truncated = decodedValue.substring(0, 97) + '...';
container.innerHTML = '<strong>' + key + ':</strong> ' + truncated + ' <span class="expand-btn" data-key="' + key + '" data-value="' + value + '" onclick="toggleExpand(this)">expand</span>';
}
}
function sendMessage() {
const text = messageInput.value.trim();
if (text || attachedImages.length > 0) {
const msg = {
type: 'sendMessage',
text: text,
planMode: planModeEnabled,
thinkingMode: thinkingModeEnabled
};
if (attachedImages.length > 0) {
msg.images = attachedImages.map(img => img.filePath);
}
vscode.postMessage(msg);
messageInput.value = '';
attachedImages = [];
renderImagePreviews();
}
}
function togglePlanMode() {
planModeEnabled = !planModeEnabled;
const switchElement = document.getElementById('planModeSwitch');
if (planModeEnabled) {
switchElement.classList.add('active');
} else {
switchElement.classList.remove('active');
}
}
function toggleThinkingMode() {
thinkingModeEnabled = !thinkingModeEnabled;
sendStats('Thinking mode toggled', { enabled: thinkingModeEnabled });
var switchElement = document.getElementById('thinkingModeSwitch');
var toggleLabel = document.getElementById('thinkingModeLabel');
var thinkBtn = document.getElementById('thinkToggleBtn');
if (thinkingModeEnabled) {
if (switchElement) switchElement.classList.add('active');
if (thinkBtn) thinkBtn.classList.add('active');
if (toggleLabel) toggleLabel.textContent = 'Ultrathink Mode';
// Set ultrathink intensity directly
vscode.postMessage({
type: 'updateSettings',
settings: { 'thinking.intensity': 'ultrathink' }
});
vscode.postMessage({
type: 'showInfoMessage',
message: 'Ultrathink enabled \u2014 deep reasoning for complex tasks.'
});
} else {
if (switchElement) switchElement.classList.remove('active');
if (thinkBtn) thinkBtn.classList.remove('active');
if (toggleLabel) toggleLabel.textContent = 'Thinking Mode';
}
}
function toggleConnectMenu() {
var menu = document.getElementById('connectMenu');
if (!menu) return;
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
}
function hideConnectMenu() {
var menu = document.getElementById('connectMenu');
if (menu) menu.style.display = 'none';
}
// Close connect menu when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.connect-dropdown-wrapper')) {
hideConnectMenu();
}
});
function cyclePlanMode() {
planModeEnabled = !planModeEnabled;
sendStats('Plan mode toggled', { enabled: planModeEnabled });
var switchElement = document.getElementById('planModeSwitch');
var toggleBtn = document.getElementById('planToggleBtn');
if (planModeEnabled) {
if (switchElement) switchElement.classList.add('active');
if (toggleBtn) toggleBtn.classList.add('active');
vscode.postMessage({
type: 'showInfoMessage',
message: 'Plan mode enabled \u2014 Claude will plan before making changes.'
});
} else {
if (switchElement) switchElement.classList.remove('active');
if (toggleBtn) toggleBtn.classList.remove('active');
}
}
let totalCost = 0;
let totalTokensInput = 0;
let totalTokensOutput = 0;
let requestCount = 0;
let isProcessing = false;
let requestStartTime = null;
let requestTimer = null;
let subscriptionType = null; // 'pro', 'max', or null for API users
// Send usage statistics
function sendStats(eventName, properties) {
${isTelemetryEnabled ?
`try {
if (typeof umami !== 'undefined' && umami.track) {
umami.track(eventName, properties);
}
} catch (error) {
console.error('Error sending stats:', error);
}` :
`// Telemetry disabled - no tracking`}
}
function updateStatus(text, state = 'ready') {
statusTextDiv.textContent = text;
statusDiv.className = \`status \${state}\`;
}
function updateStatusHtml(html, state = 'ready') {
statusTextDiv.innerHTML = html;
statusDiv.className = \`status \${state}\`;
}
function viewUsage(usageType) {
vscode.postMessage({ type: 'viewUsage', usageType: usageType });
}
function updateStatusWithTotals() {
if (isProcessing) {
// While processing, show elapsed time (and tokens for non-OpenCredits users)
let elapsedStr = '';
if (requestStartTime) {
const elapsedSeconds = Math.floor((Date.now() - requestStartTime) / 1000);
elapsedStr = \`\${elapsedSeconds}s\`;
}
let statusText;
if (hasOpenCreditsKey) {
// OpenCredits users: don't show tokens, just elapsed time
statusText = \`Processing\${elapsedStr ? \`\${elapsedStr}\` : ''}\`;
} else {
// Regular users: show tokens and elapsed time
const totalTokens = totalTokensInput + totalTokensOutput;
const tokensStr = totalTokens > 0 ?
\`\${totalTokens.toLocaleString()} tokens\` : '0 tokens';
statusText = \`Processing • \${tokensStr}\${elapsedStr ? \`\${elapsedStr}\` : ''}\`;
}
updateStatus(statusText, 'processing');
} else {
// When ready, show full info
let usageStr;
const usageIcon = \`<svg class="usage-icon" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="8" width="3" height="6" rx="0.5" fill="currentColor" opacity="0.5"/>
<rect x="5.5" y="5" width="3" height="9" rx="0.5" fill="currentColor" opacity="0.7"/>
<rect x="10" y="2" width="3" height="12" rx="0.5" fill="currentColor"/>
</svg>\`;
if (hasOpenCreditsKey && openCreditsBalance !== null) {
// OpenCredits user with OpenCredits model selected: show balance
const balanceStr = \`Balance: \${openCreditsBalance.toFixed(1)} credits\`;
usageStr = \`<a href="#" onclick="event.preventDefault(); OpenCredits.open();" class="usage-badge opencredits-balance" title="Open OpenCredits Account">\${balanceStr}</a>\`;
} else if (subscriptionType) {
// Plan subscriber: show plan type
let planName = subscriptionType.replace(/^claude\\s*/i, '').trim();
planName = planName.charAt(0).toUpperCase() + planName.slice(1);
usageStr = \`<a href="#" onclick="event.preventDefault(); viewUsage('plan');" class="usage-badge" title="View live usage">\${planName} Plan\${usageIcon}</a>\`;
} else {
// API user: show cost
const costStr = totalCost > 0 ? \`$\${totalCost.toFixed(4)}\` : '$0.00';
usageStr = \`<a href="#" onclick="event.preventDefault(); viewUsage('api');" class="usage-badge" title="View usage">\${costStr}\${usageIcon}</a>\`;
}
let statusText;
if (hasOpenCreditsKey) {
// OpenCredits users with OpenCredits model: just show ready and balance (no tokens)
const requestStr = requestCount > 0 ? \`\${requestCount} requests\` : '';
statusText = \`Ready\${requestStr ? \`\${requestStr}\` : ''} • \${usageStr}\`;
} else {
// Regular users: show tokens, requests, and usage
const totalTokens = totalTokensInput + totalTokensOutput;
const tokensStr = totalTokens > 0 ?
\`\${totalTokens.toLocaleString()} tokens\` : '0 tokens';
const requestStr = requestCount > 0 ? \`\${requestCount} requests\` : '';
statusText = \`Ready • \${tokensStr}\${requestStr ? \`\${requestStr}\` : ''} • \${usageStr}\`;
}
updateStatusHtml(statusText, 'ready');
}
}
function startRequestTimer(startTime = undefined) {
requestStartTime = startTime || Date.now();
// Update status every 100ms for smooth real-time display
requestTimer = setInterval(() => {
if (isProcessing) {
updateStatusWithTotals();
}
}, 100);
}
function stopRequestTimer() {
if (requestTimer) {
clearInterval(requestTimer);
requestTimer = null;
}
requestStartTime = null;
}
function showProcessingIndicator() {
// Remove any existing indicator first
hideProcessingIndicator();
// Create the indicator and append after all messages
const indicator = document.createElement('div');
indicator.className = 'processing-indicator';
indicator.innerHTML = '<div class="morph-dot"></div>';
messagesDiv.appendChild(indicator);
}
function hideProcessingIndicator() {
const indicator = document.querySelector('.processing-indicator');
if (indicator) {
indicator.remove();
}
}
function moveProcessingIndicatorToLast() {
// Only move if we're processing
if (isProcessing) {
showProcessingIndicator();
}
}
// Auto-resize textarea
function adjustTextareaHeight() {
// Reset height to calculate new height
messageInput.style.height = 'auto';
// Get computed styles
const computedStyle = getComputedStyle(messageInput);
const lineHeight = parseFloat(computedStyle.lineHeight);
const paddingTop = parseFloat(computedStyle.paddingTop);
const paddingBottom = parseFloat(computedStyle.paddingBottom);
const borderTop = parseFloat(computedStyle.borderTopWidth);
const borderBottom = parseFloat(computedStyle.borderBottomWidth);
// Calculate heights
const scrollHeight = messageInput.scrollHeight;
const maxRows = 5;
const minHeight = lineHeight + paddingTop + paddingBottom + borderTop + borderBottom;
const maxHeight = (lineHeight * maxRows) + paddingTop + paddingBottom + borderTop + borderBottom;
// Set height
if (scrollHeight <= maxHeight) {
messageInput.style.height = Math.max(scrollHeight, minHeight) + 'px';
messageInput.style.overflowY = 'hidden';
} else {
messageInput.style.height = maxHeight + 'px';
messageInput.style.overflowY = 'auto';
}
}
messageInput.addEventListener('input', adjustTextareaHeight);
// Save input text as user types (debounced)
let saveInputTimeout;
messageInput.addEventListener('input', () => {
clearTimeout(saveInputTimeout);
saveInputTimeout = setTimeout(() => {
vscode.postMessage({
type: 'saveInputText',
text: messageInput.value
});
}, 500); // Save after 500ms of no typing
});
messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const sendBtn = document.getElementById('sendBtn');
if (sendBtn.disabled){
return;
}
sendMessage();
} else if (e.key === '@' && !e.ctrlKey && !e.metaKey) {
// Don't prevent default, let @ be typed first
setTimeout(() => {
showFilePicker();
}, 0);
} else if (e.key === 'Escape' && filePickerModal.style.display === 'flex') {
e.preventDefault();
hideFilePicker();
} else if (e.key === 'v' && (e.ctrlKey || e.metaKey)) {
// Handle Ctrl+V/Cmd+V explicitly in case paste event doesn't fire
// Don't prevent default - let browser handle it first
setTimeout(() => {
// If value hasn't changed, manually trigger paste
const currentValue = messageInput.value;
setTimeout(() => {
if (messageInput.value === currentValue) {
// Value didn't change, request clipboard from VS Code
vscode.postMessage({
type: 'getClipboardText'
});
}
}, 50);
}, 0);
}
});
// Add explicit paste event handler for better clipboard support in VSCode webviews
messageInput.addEventListener('paste', async (e) => {
e.preventDefault();
try {
// Try to get clipboard data from the event first
const clipboardData = e.clipboardData;
// Check for images first
if (clipboardData && clipboardData.items) {
let hasImage = false;
for (let i = 0; i < clipboardData.items.length; i++) {
const item = clipboardData.items[i];
if (item.type.startsWith('image/')) {
// Found an image, handle it
hasImage = true;
const blob = item.getAsFile();
if (blob) {
// Convert blob to base64
const reader = new FileReader();
reader.onload = function(event) {
const base64Data = event.target.result;
// Send to extension to create file
vscode.postMessage({
type: 'createImageFile',
imageData: base64Data,
imageType: item.type
});
};
reader.readAsDataURL(blob);
}
break; // Process only the first image found
}
}
// If we found an image, don't process any text
if (hasImage) {
return;
}
}
// No image found, handle text
let text = '';
if (clipboardData) {
text = clipboardData.getData('text/plain');
}
// If no text from event, try navigator.clipboard API
if (!text && navigator.clipboard && navigator.clipboard.readText) {
try {
text = await navigator.clipboard.readText();
} catch (err) {
console.error('Clipboard API failed:', err);
}
}
// If still no text, request from VS Code extension
if (!text) {
vscode.postMessage({
type: 'getClipboardText'
});
return;
}
// Insert text at cursor position
const start = messageInput.selectionStart;
const end = messageInput.selectionEnd;
const currentValue = messageInput.value;
const newValue = currentValue.substring(0, start) + text + currentValue.substring(end);
messageInput.value = newValue;
// Set cursor position after pasted text
const newCursorPos = start + text.length;
messageInput.setSelectionRange(newCursorPos, newCursorPos);
// Trigger input event to adjust height
messageInput.dispatchEvent(new Event('input', { bubbles: true }));
} catch (error) {
console.error('Paste error:', error);
}
});
// Handle context menu paste
messageInput.addEventListener('contextmenu', (e) => {
// Don't prevent default - allow context menu to show
// but ensure paste will work when selected
});
// Initialize textarea height
adjustTextareaHeight();
// File picker event listeners
fileSearchInput.addEventListener('input', (e) => {
filterFiles(e.target.value);
});
fileSearchInput.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedFileIndex = Math.min(selectedFileIndex + 1, filteredFiles.length - 1);
renderFileList();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedFileIndex = Math.max(selectedFileIndex - 1, -1);
renderFileList();
} else if (e.key === 'Enter' && selectedFileIndex >= 0) {
e.preventDefault();
selectFile(filteredFiles[selectedFileIndex]);
} else if (e.key === 'Escape') {
e.preventDefault();
hideFilePicker();
}
});
// Close modal when clicking outside
filePickerModal.addEventListener('click', (e) => {
if (e.target === filePickerModal) {
hideFilePicker();
}
});
// Tools modal functions
function showMCPModal() {
sendStats('MCP modal opened');
document.getElementById('mcpModal').style.display = 'flex';
loadMCPServers();
if (!marketplaceCache || marketplaceCache.length === 0) {
loadMarketplace();
}
}
function updateYoloWarning() {
const yoloModeCheckbox = document.getElementById('yolo-mode');
const warning = document.getElementById('yoloWarning');
if (!yoloModeCheckbox || !warning) {
return; // Elements not ready yet
}
const yoloMode = yoloModeCheckbox.checked;
warning.style.display = yoloMode ? 'block' : 'none';
}
function isPermissionError(content) {
const permissionErrorPatterns = [
'Error: MCP config file not found',
'Error: MCP tool',
'Claude requested permissions to use',
'permission denied',
'Permission denied',
'permission request',
'Permission request',
'EACCES',
'permission error',
'Permission error'
];
return permissionErrorPatterns.some(pattern =>
content.toLowerCase().includes(pattern.toLowerCase())
);
}
function enableYoloMode() {
sendStats('YOLO mode enabled');
// Update the checkbox
const yoloModeCheckbox = document.getElementById('yolo-mode');
if (yoloModeCheckbox) {
yoloModeCheckbox.checked = true;
// Trigger the settings update
updateSettings();
// Show confirmation message
addMessage('✅ Yolo Mode enabled! All permission checks will be bypassed for future commands.', 'system');
// Update the warning banner
updateYoloWarning();
}
}
function hideMCPModal() {
document.getElementById('mcpModal').style.display = 'none';
hideAddServerForm();
}
// Close MCP modal when clicking outside
document.getElementById('mcpModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('mcpModal')) {
hideMCPModal();
}
});
// MCP Server management functions
function loadMCPServers() {
vscode.postMessage({ type: 'loadMCPServers' });
}
function showAddServerForm() {
document.getElementById('mcpServersList').style.display = 'none';
document.getElementById('popularServers').style.display = 'none';
document.getElementById('addServerForm').style.display = 'block';
}
function hideAddServerForm() {
document.getElementById('mcpServersList').style.display = '';
document.getElementById('popularServers').style.display = 'block';
document.getElementById('addServerForm').style.display = 'none';
loadMCPServers();
// Reset editing state
editingServerName = null;
// Reset form title and button
const formTitle = document.querySelector('#addServerForm h5');
if (formTitle) formTitle.remove();
const saveBtn = document.querySelector('#addServerForm .btn:not(.outlined)');
if (saveBtn) saveBtn.textContent = 'Add Server';
// Clear form
document.getElementById('serverName').value = '';
document.getElementById('serverName').disabled = false;
document.getElementById('serverCommand').value = '';
document.getElementById('serverUrl').value = '';
document.getElementById('serverArgs').value = '';
document.getElementById('serverEnv').value = '';
document.getElementById('serverHeaders').value = '';
document.getElementById('serverType').value = 'http';
updateServerForm();
}
function updateServerForm() {
const serverType = document.getElementById('serverType').value;
const commandGroup = document.getElementById('commandGroup');
const urlGroup = document.getElementById('urlGroup');
const argsGroup = document.getElementById('argsGroup');
const envGroup = document.getElementById('envGroup');
const headersGroup = document.getElementById('headersGroup');
if (serverType === 'stdio') {
commandGroup.style.display = 'block';
urlGroup.style.display = 'none';
argsGroup.style.display = 'block';
envGroup.style.display = 'block';
headersGroup.style.display = 'none';
} else if (serverType === 'http' || serverType === 'sse') {
commandGroup.style.display = 'none';
urlGroup.style.display = 'block';
argsGroup.style.display = 'none';
envGroup.style.display = 'none';
headersGroup.style.display = 'block';
}
}
function saveMCPServer() {
const name = document.getElementById('serverName').value.trim();
const type = document.getElementById('serverType').value;
if (!name) {
// Use a simple notification instead of alert which is blocked
const notification = document.createElement('div');
notification.textContent = 'Server name is required';
notification.style.cssText = 'position: fixed; top: 20px; right: 20px; background: var(--vscode-inputValidation-errorBackground); color: var(--vscode-inputValidation-errorForeground); padding: 8px 12px; border-radius: 4px; z-index: 9999;';
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
return;
}
// If editing, we can use the same name; if adding, check for duplicates
if (!editingServerName) {
const serversList = document.getElementById('mcpServersList');
const existingServers = serversList.querySelectorAll('.server-name');
for (let server of existingServers) {
if (server.textContent === name) {
const notification = document.createElement('div');
notification.textContent = \`Server "\${name}" already exists\`;
notification.style.cssText = 'position: fixed; top: 20px; right: 20px; background: var(--vscode-inputValidation-errorBackground); color: var(--vscode-inputValidation-errorForeground); padding: 8px 12px; border-radius: 4px; z-index: 9999;';
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
return;
}
}
}
const serverConfig = { type };
if (type === 'stdio') {
const command = document.getElementById('serverCommand').value.trim();
if (!command) {
const notification = document.createElement('div');
notification.textContent = 'Command is required for stdio servers';
notification.style.cssText = 'position: fixed; top: 20px; right: 20px; background: var(--vscode-inputValidation-errorBackground); color: var(--vscode-inputValidation-errorForeground); padding: 8px 12px; border-radius: 4px; z-index: 9999;';
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
return;
}
serverConfig.command = command;
const argsText = document.getElementById('serverArgs').value.trim();
if (argsText) {
serverConfig.args = argsText.split('\\n').filter(line => line.trim());
}
const envText = document.getElementById('serverEnv').value.trim();
if (envText) {
serverConfig.env = {};
envText.split('\\n').forEach(line => {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
serverConfig.env[key.trim()] = valueParts.join('=').trim();
}
});
}
} else if (type === 'http' || type === 'sse') {
const url = document.getElementById('serverUrl').value.trim();
if (!url) {
const notification = document.createElement('div');
notification.textContent = 'URL is required for HTTP/SSE servers';
notification.style.cssText = 'position: fixed; top: 20px; right: 20px; background: var(--vscode-inputValidation-errorBackground); color: var(--vscode-inputValidation-errorForeground); padding: 8px 12px; border-radius: 4px; z-index: 9999;';
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
return;
}
serverConfig.url = url;
const headersText = document.getElementById('serverHeaders').value.trim();
if (headersText) {
serverConfig.headers = {};
headersText.split('\\n').forEach(line => {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
serverConfig.headers[key.trim()] = valueParts.join('=').trim();
}
});
}
}
var scope = document.getElementById('serverScope') ? document.getElementById('serverScope').value : 'project';
sendStats('MCP server added', { name: name });
vscode.postMessage({
type: 'saveMCPServer',
name: name,
config: serverConfig,
scope: scope
});
hideAddServerForm();
}
function authenticateMCPs() {
vscode.postMessage({ type: 'runTerminalCommand', command: 'claude /mcp' });
}
function deleteMCPServer(serverName, scope) {
sendStats('MCP server removed', { name: serverName });
vscode.postMessage({
type: 'deleteMCPServer',
name: serverName,
scope: scope || 'project'
});
}
let editingServerName = null;
function editMCPServer(name, config) {
editingServerName = name;
// Hide add button and popular servers
document.getElementById('addServerBtn').style.display = 'none';
document.getElementById('popularServers').style.display = 'none';
// Show form
document.getElementById('addServerForm').style.display = 'block';
// Update form title and button
const formTitle = document.querySelector('#addServerForm h5') ||
document.querySelector('#addServerForm').insertAdjacentHTML('afterbegin', '<h5>Edit MCP Server</h5>') ||
document.querySelector('#addServerForm h5');
if (!document.querySelector('#addServerForm h5')) {
document.getElementById('addServerForm').insertAdjacentHTML('afterbegin', '<h5 style="margin: 0 0 20px 0; font-size: 14px; font-weight: 600;">Edit MCP Server</h5>');
} else {
document.querySelector('#addServerForm h5').textContent = 'Edit MCP Server';
}
// Update save button text
const saveBtn = document.querySelector('#addServerForm .btn:not(.outlined)');
if (saveBtn) saveBtn.textContent = 'Update Server';
// Populate form with existing values
document.getElementById('serverName').value = name;
document.getElementById('serverName').disabled = true; // Don't allow name changes when editing
document.getElementById('serverType').value = config.type || 'stdio';
if (config.command) {
document.getElementById('serverCommand').value = config.command;
}
if (config.url) {
document.getElementById('serverUrl').value = config.url;
}
if (config.args && Array.isArray(config.args)) {
document.getElementById('serverArgs').value = config.args.join('\\n');
}
if (config.env) {
const envLines = Object.entries(config.env).map(([key, value]) => \`\${key}=\${value}\`);
document.getElementById('serverEnv').value = envLines.join('\\n');
}
if (config.headers) {
const headerLines = Object.entries(config.headers).map(([key, value]) => \`\${key}=\${value}\`);
document.getElementById('serverHeaders').value = headerLines.join('\\n');
}
// Update form field visibility
updateServerForm();
const toolsList = document.querySelector('.tools-list');
if (toolsList) {
toolsList.scrollTop = toolsList.scrollHeight;
}
}
function addPopularServer(name, config, scope) {
// Check if server already exists
const serversList = document.getElementById('mcpServersList');
const existingServers = serversList.querySelectorAll('.server-name');
for (let server of existingServers) {
if (server.textContent === name) {
const notification = document.createElement('div');
notification.textContent = \`Server "\${name}" already exists\`;
notification.style.cssText = 'position: fixed; top: 20px; right: 20px; background: var(--vscode-inputValidation-errorBackground); color: var(--vscode-inputValidation-errorForeground); padding: 8px 12px; border-radius: 4px; z-index: 9999;';
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
return;
}
}
sendStats('MCP server added', { name: name });
// Add the server
vscode.postMessage({
type: 'saveMCPServer',
name: name,
config: config,
scope: scope || 'project'
});
}
// MCP Marketplace
var marketplaceCache = null;
var marketplaceDisplayed = null;
var marketplaceCursor = null;
var marketplaceSearchTimeout = null;
var pendingRegistryResponses = 0;
var accumulatedRegistryResults = [];
// Top MCP servers (static) + MCP Registry search
var topMcpServers = (window.__topMcpServers || []);
function loadMarketplace() {
marketplaceCache = topMcpServers;
renderMarketplace(marketplaceCache);
// Also fetch fresh data from curated registry
vscode.postMessage({ type: 'marketplaceFetch', url: 'https://mcp.agent-tooling.dev/api/v1/servers?version=latest&limit=50', isSearch: false });
}
function loadMoreMarketplace() {}
function filterMarketplace(query) {
clearTimeout(marketplaceSearchTimeout);
marketplaceSearchTimeout = setTimeout(function() {
if (!query) {
lastSearchQuery = '';
pendingRegistryResponses = 0;
accumulatedRegistryResults = [];
marketplaceCache = topMcpServers;
renderMarketplace(marketplaceCache);
return;
}
// Show loading, then accumulate from both registries
lastSearchQuery = query;
accumulatedRegistryResults = [];
pendingRegistryResponses = 2;
renderMarketplace([], true);
vscode.postMessage({ type: 'marketplaceFetch', url: 'https://mcp.agent-tooling.dev/api/v1/servers?version=latest&limit=50&search=' + encodeURIComponent(query), isSearch: true });
vscode.postMessage({ type: 'marketplaceFetch', url: 'https://registry.modelcontextprotocol.io/v0.1/servers?version=latest&limit=50&search=' + encodeURIComponent(query), isSearch: true });
}, 300);
}
// Trusted namespace prefixes for ranking (official/vendor servers)
var TRUSTED_PREFIXES = ['com.supabase/', 'io.github.github/', 'com.stripe/', 'com.vercel/', 'io.github.vercel/', 'com.notion/', 'app.linear/', 'com.atlassian/', 'com.cloudflare.', 'io.github.getsentry/', 'io.github.mongodb-js/', 'io.github.railwayapp/', 'com.postman/', 'com.slack/', 'com.neon/', 'com.figma/', 'dev.firecrawl/', 'com.netlify/', 'com.resend/', 'ai.exa/', 'com.airtable/', 'com.apify/', 'com.mux/', 'com.render/'];
var SMITHERY_PREFIX = 'ai.smithery/';
function rankRegistryResult(query, entry) {
var q = (query || '').trim().toLowerCase();
var name = (entry.id || '').toLowerCase();
var title = (entry.name || '').toLowerCase();
var desc = (entry.description || '').toLowerCase();
var haystack = name + ' ' + title + ' ' + desc;
var score = 0;
if (q) {
if (name === q) score += 800;
if (name.indexOf(q) >= 0) score += 350;
if (title.indexOf(q) >= 0) score += 250;
if (desc.indexOf(q) >= 0) score += 120;
var tokens = q.split(/[^a-z0-9]+/).filter(function(t) { return t.length > 0; });
var haystackTokens = haystack.split(/[^a-z0-9]+/).filter(function(t) { return t.length > 0; });
var haystackSet = {};
haystackTokens.forEach(function(t) { haystackSet[t] = true; });
tokens.forEach(function(t) {
if (haystackSet[t]) score += 60;
else if (t.length >= 3 && haystack.indexOf(t) >= 0) score += 20;
});
}
// Prefer trusted namespaces
for (var i = 0; i < TRUSTED_PREFIXES.length; i++) {
if (name.indexOf(TRUSTED_PREFIXES[i]) === 0) { score += 500; break; }
}
// Demote smithery aggregators
if (name.indexOf(SMITHERY_PREFIX) === 0) score -= 500;
return score;
}
function parseRegistryEntry(item) {
var s = item.server || item;
var meta = item._meta || {};
var status = (meta['io.modelcontextprotocol.registry/official'] || {}).status || 'active';
if (status !== 'active') return null;
var packages = s.packages || [];
var remotes = s.remotes || [];
if (!packages.length && !remotes.length) return null;
var installConfig = null;
var installType = '';
if (remotes.length > 0) {
var remote = remotes[0];
var rtype = remote.type === 'streamable-http' ? 'http' : (remote.type || 'http');
installType = rtype;
installConfig = { type: rtype === 'sse' ? 'sse' : 'http', url: remote.url };
if (remote.headers && remote.headers.length > 0) {
var headers = {};
remote.headers.forEach(function(h) { if (h.name) headers[h.name] = ''; });
if (Object.keys(headers).length > 0) installConfig.headers = headers;
}
} else if (packages.length > 0) {
var pkg = packages[0];
installType = pkg.registryType || 'npm';
if (pkg.registryType === 'npm') {
installConfig = { type: 'stdio', command: 'npx', args: ['-y', pkg.identifier || s.name] };
} else if (pkg.registryType === 'oci' || pkg.registryType === 'docker') {
installConfig = { type: 'stdio', command: 'docker', args: ['run', '-i', '--rm', pkg.identifier || s.name] };
} else {
installConfig = { type: 'stdio', command: 'npx', args: ['-y', pkg.identifier || s.name] };
}
if (pkg.environmentVariables && pkg.environmentVariables.length > 0) {
var env = {};
pkg.environmentVariables.forEach(function(ev) {
if (ev.name) env[ev.name] = ev.default || '';
});
if (Object.keys(env).length > 0) installConfig.env = env;
}
}
return {
id: s.name || '',
name: (s.title || s.name || '').split('/').pop(),
description: s.description || '',
icon: '',
stars: 0,
url: (s.repository && s.repository.url) || s.websiteUrl || '',
installType: installType,
installConfig: installConfig
};
}
var lastSearchQuery = '';
function handleMarketplaceResponse(data, searchQuery) {
// Parse entries from this response
(data.servers || []).forEach(function(item) {
var entry = parseRegistryEntry(item);
if (entry) accumulatedRegistryResults.push(entry);
});
// For non-search (browse refresh), merge immediately
if (!data._isSearch) {
var topIds = topMcpServers.map(function(s) { return s.id; });
var extra = accumulatedRegistryResults.filter(function(s) {
return topIds.indexOf(s.id) < 0;
});
if (extra.length > 0) {
renderMarketplace((marketplaceDisplayed || []).concat(extra));
}
return;
}
// For search, wait for all registries to respond
pendingRegistryResponses--;
if (pendingRegistryResponses > 0) return;
// All responses received — dedupe, filter, rank, merge
var deduped = {};
var allResults = [];
accumulatedRegistryResults.forEach(function(entry) {
var key = entry.id + '@' + (entry.version || '0');
if (!deduped[key]) {
deduped[key] = true;
allResults.push(entry);
}
});
// Filter smithery when alternatives exist
var hasNonSmithery = allResults.some(function(s) { return s.id.indexOf(SMITHERY_PREFIX) !== 0; });
if (hasNonSmithery) {
allResults = allResults.filter(function(s) { return s.id.indexOf(SMITHERY_PREFIX) !== 0; });
}
// Dedupe against local results
var localIds = (marketplaceDisplayed || []).map(function(s) { return s.id; });
var extra = allResults.filter(function(s) { return localIds.indexOf(s.id) < 0; });
// Merge and rank the full list
var merged = (marketplaceDisplayed || []).concat(extra);
if (searchQuery) {
merged.sort(function(a, b) {
var diff = rankRegistryResult(searchQuery, b) - rankRegistryResult(searchQuery, a);
if (diff !== 0) return diff;
return (a.id || '').localeCompare(b.id || '');
});
}
renderMarketplace(merged);
}
function renderMarketplace(servers, isLoading) {
marketplaceDisplayed = servers;
var grid = document.getElementById('marketplaceGrid');
if ((!servers || servers.length === 0) && !isLoading) {
grid.innerHTML = '<div class="marketplace-loading">No servers found.</div>';
return;
}
var html = '';
(servers || []).forEach(function(server) {
var name = server.name || 'Unknown';
var desc = escapeHtml(server.description || 'No description');
var icon = server.icon || '';
var stars = server.stars || 0;
var installType = server.installType || '';
var iconHtml = icon ? '<img src="' + escapeHtml(icon) + '" class="marketplace-item-icon" onerror="this.style.display=&quot;none&quot;" />' : '<div class="marketplace-item-icon-placeholder">' + escapeHtml(name.charAt(0).toUpperCase()) + '</div>';
var starsHtml = stars > 0 ? '<span class="marketplace-item-stars">' + (stars >= 1000 ? (Math.round(stars / 100) / 10) + 'k' : stars) + ' &#9733;</span>' : '';
var typeHtml = installType ? '<span class="marketplace-item-type">' + escapeHtml(installType) + '</span>' : '';
var safeId = escapeHtml(server.id || name).replace(/'/g, '&#39;');
html += '<div class="marketplace-item" data-server="' + safeId + '" onclick="showMarketplaceDetail(this.dataset.server)">' +
'<div class="marketplace-item-header">' +
iconHtml +
'<div class="marketplace-item-info">' +
'<div class="marketplace-item-name">' + escapeHtml(name) + (server.featured ? ' <span style="color:#f59e0b;font-size:10px;" title="Featured">&#9733;</span>' : '') + '</div>' +
'<div class="marketplace-item-meta">' + starsHtml + typeHtml + '</div>' +
'</div>' +
'</div>' +
'<div class="marketplace-item-desc">' + desc + '</div>' +
'</div>';
});
if (isLoading) {
html += '<div class="marketplace-loading" style="padding: 16px; text-align: center; opacity: 0.6; font-size: 12px;">Searching registries...</div>';
} else if (servers && servers.length > 0) {
var btnLabel = lastSearchQuery ? 'Search for more results' : 'Browse more servers';
html += '<div style="padding: 12px; text-align: center;"><button onclick="focusMarketplaceSearch()" style="background: none; border: 1px solid var(--vscode-panel-border); color: var(--vscode-descriptionForeground); padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 12px;">' + btnLabel + '</button></div>';
}
grid.innerHTML = html;
}
function searchMoreResults() {
var query = lastSearchQuery;
if (!query) return;
pendingRegistryResponses = 2;
accumulatedRegistryResults = [];
renderMarketplace(marketplaceDisplayed, true);
vscode.postMessage({ type: 'marketplaceFetch', url: 'https://mcp.agent-tooling.dev/api/v1/servers?version=latest&limit=100&search=' + encodeURIComponent(query), isSearch: true });
vscode.postMessage({ type: 'marketplaceFetch', url: 'https://registry.modelcontextprotocol.io/v0.1/servers?version=latest&limit=100&search=' + encodeURIComponent(query), isSearch: true });
}
function focusMarketplaceSearch() {
var input = document.getElementById('marketplaceSearch');
if (input) {
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
input.focus();
}
}
function showMarketplaceDetail(serverId) {
var server = (marketplaceDisplayed || []).find(function(s) { return s.id === serverId; }) ||
(marketplaceCache || []).find(function(s) { return s.id === serverId; });
if (!server) return;
var name = server.name || 'Unknown';
var desc = server.description || 'No description available.';
var icon = server.icon || '';
var stars = server.stars || 0;
var url = server.url || '';
var cfg = server.installConfig;
var iconHtml = icon ? '<img src="' + escapeHtml(icon) + '" class="marketplace-detail-icon" onerror="this.style.display=&quot;none&quot;" />' : '<div class="marketplace-item-icon-placeholder" style="width:40px;height:40px;font-size:18px;">' + escapeHtml(name.charAt(0).toUpperCase()) + '</div>';
var starsHtml = stars > 0 ? '<span class="marketplace-item-stars">' + (stars >= 1000 ? (Math.round(stars / 100) / 10) + 'k' : stars) + ' &#9733;</span>' : '';
// Build install info from installConfig
var installInfo = '';
if (cfg) {
installInfo = '<div class="marketplace-detail-section-title">Configuration</div>';
if (cfg.type === 'stdio') {
installInfo += '<div class="marketplace-detail-row"><span class="detail-label">Command:</span> <code>' + escapeHtml(cfg.command + ' ' + (cfg.args || []).join(' ')) + '</code></div>';
installInfo += '<div class="marketplace-detail-row"><span class="detail-label">Type:</span> stdio</div>';
} else {
installInfo += '<div class="marketplace-detail-row"><span class="detail-label">URL:</span> <code>' + escapeHtml(cfg.url || '') + '</code></div>';
installInfo += '<div class="marketplace-detail-row"><span class="detail-label">Type:</span> ' + escapeHtml(cfg.type || 'http') + '</div>';
}
if (cfg.env) {
installInfo += '<div class="marketplace-detail-row"><span class="detail-label">Env vars:</span></div>';
for (var envKey in cfg.env) {
if (cfg.env.hasOwnProperty(envKey)) {
installInfo += '<div class="marketplace-detail-env"><code>' + escapeHtml(envKey) + '</code></div>';
}
}
}
}
var safeId = escapeHtml(serverId).replace(/'/g, '&#39;');
var grid = document.getElementById('marketplaceGrid');
var loadMoreBtn = document.getElementById('marketplaceLoadMore');
if (loadMoreBtn) loadMoreBtn.style.display = 'none';
grid.innerHTML = '<div class="marketplace-detail">' +
'<button class="marketplace-back-btn" onclick="renderMarketplace(marketplaceDisplayed || marketplaceCache)">&#8592; Back</button>' +
'<div class="marketplace-detail-header">' +
iconHtml +
'<div class="marketplace-detail-header-info">' +
'<div class="marketplace-detail-name">' + escapeHtml(name) + '</div>' +
'<div class="marketplace-detail-header-meta">' +
starsHtml +
(url ? '<a href="' + escapeHtml(url) + '" target="_blank" class="marketplace-detail-link">GitHub</a>' : '') +
'</div>' +
'</div>' +
(cfg ? '<div style="display:flex;align-items:center;gap:8px;margin-left:auto;"><select id="mcpInstallScope" style="padding:4px 6px;background:var(--vscode-input-background);color:var(--vscode-input-foreground);border:1px solid var(--vscode-input-border);border-radius:4px;font-size:11px;"><option value="project">Project (.mcp.json)</option><option value="global">Global (~/.claude.json)</option></select><button class="btn marketplace-install-btn" data-server="' + safeId + '" onclick="installMarketplaceServer(this.dataset.server)">Install</button></div>' : '') +
'</div>' +
'<div class="marketplace-detail-desc">' + escapeHtml(desc) + '</div>' +
(installInfo ? '<div class="marketplace-detail-config">' + installInfo + '</div>' : '') +
(!cfg ? '<div class="marketplace-detail-row" style="color:var(--vscode-descriptionForeground)">No install config available.</div>' : '') +
'</div>';
}
function installMarketplaceServer(serverId) {
var server = (marketplaceDisplayed || []).find(function(s) { return s.id === serverId; }) ||
(marketplaceCache || []).find(function(s) { return s.id === serverId; });
if (!server || !server.installConfig) return;
var cfg = server.installConfig;
var displayName = server.name || serverId.split('/').pop() || serverId;
// Get selected scope from detail view
var scopeSelect = document.getElementById('mcpInstallScope');
var selectedScope = scopeSelect ? scopeSelect.value : 'project';
// Pre-fill the manual add form with the config
showAddServerForm();
var formScope = document.getElementById('serverScope');
if (formScope) formScope.value = selectedScope;
document.getElementById('serverName').value = displayName;
document.getElementById('serverName').disabled = false;
if (cfg.type === 'stdio') {
document.getElementById('serverType').value = 'stdio';
updateServerForm();
document.getElementById('serverCommand').value = cfg.command || '';
document.getElementById('serverArgs').value = (cfg.args || []).join('\\n');
if (cfg.env) {
var envLines = [];
for (var k in cfg.env) {
if (cfg.env.hasOwnProperty(k)) {
envLines.push(k + '=' + cfg.env[k]);
}
}
document.getElementById('serverEnv').value = envLines.join('\\n');
}
} else {
document.getElementById('serverType').value = cfg.type === 'sse' ? 'sse' : 'http';
updateServerForm();
document.getElementById('serverUrl').value = cfg.url || '';
}
// Scroll to the form
var form = document.getElementById('addServerForm');
if (form) form.scrollIntoView({ behavior: 'smooth' });
}
function displayMCPServers(servers) {
const serversList = document.getElementById('mcpServersList');
serversList.innerHTML = '';
if (Object.keys(servers).length === 0) {
serversList.innerHTML = '<div class="no-servers">' +
'<div class="no-servers-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1" fill="currentColor"/><circle cx="6" cy="18" r="1" fill="currentColor"/></svg></div>' +
'<div class="no-servers-text">No MCP servers configured</div>' +
'<button class="btn outlined no-servers-btn" onclick="showAddServerForm()">+ Add manually</button>' +
'</div>';
return;
}
for (const [name, config] of Object.entries(servers)) {
const serverItem = document.createElement('div');
serverItem.className = 'mcp-server-item';
// Defensive check for config structure
if (!config || typeof config !== 'object') {
console.error('Invalid config for server:', name, config);
continue;
}
const serverType = config.type || 'stdio';
const serverScope = config._scope || 'project';
let configDisplay = '';
if (serverType === 'stdio') {
configDisplay = \`Command: \${config.command || 'Not specified'}\`;
if (config.args && Array.isArray(config.args)) {
configDisplay += \`<br>Args: \${config.args.join(' ')}\`;
}
} else if (serverType === 'http' || serverType === 'sse') {
configDisplay = \`URL: \${config.url || 'Not specified'}\`;
} else {
configDisplay = \`Type: \${serverType}\`;
}
const scopeLabel = serverScope === 'global' ? 'Global' : serverScope === 'project' ? 'Project' : 'Extension';
serverItem.innerHTML = \`
<div class="server-info">
<div class="server-name">\${name} <span style="font-size:10px;opacity:0.5;font-weight:normal;">\${scopeLabel}</span></div>
<div class="server-type">\${serverType.toUpperCase()}</div>
<div class="server-config">\${configDisplay}</div>
</div>
<div class="server-actions">
<button class="btn outlined server-edit-btn" onclick="editMCPServer('\${name}', \${JSON.stringify(config).replace(/"/g, '&quot;')})">Edit</button>
<button class="btn outlined server-delete-btn" onclick="deleteMCPServer('\${name}', '\${serverScope}')">Delete</button>
</div>
\`;
serversList.appendChild(serverItem);
}
// Add buttons at the bottom
var actionsDiv = document.createElement('div');
actionsDiv.className = 'mcp-add-server';
actionsDiv.style.cssText = 'display: flex; gap: 8px; align-items: center;';
actionsDiv.innerHTML = '<button class="btn outlined" onclick="showAddServerForm()">+ Add manually</button>' +
'<span style="flex:1;"></span>' +
'<span class="mcp-auth-btn" data-tooltip="Some MCPs may require authentication. Opens a terminal to log in." onclick="authenticateMCPs()">Authenticate</span>';
serversList.appendChild(actionsDiv);
}
// Model selector functions
let currentModel = 'opus'; // Default model
let pendingModelSelection = null; // Model to activate after payment
let hasOpenCreditsKey = false; // Whether OpenCredits key exists in env vars
let openCreditsBalance = null; // OpenCredits account balance
let envsDisabled = false; // Whether custom env vars are disabled
let opencreditsEnabled = false; // Feature flag: whether OpenCredits is available in this region
let hasSavedOpenCreditsKey = false; // Whether a key exists in encrypted storage
// Recommended alternative models - loaded from recommended-models.json via window.__recommendedModels
let creditsPricingData = null; // { referenceModel, models: [{ id, credits_per_request }], tokenAssumption }
let openCreditsModels = (window.__recommendedModels || []).map(function(m) {
return {
id: m.id,
name: m.name,
provider: m.provider,
quickLabel: m.quickLabel,
credits_per_request: m.credits_per_request || null,
tierModels: m.tierModels
};
});
// Check if a model is a OpenCredits model (any model that's not a Claude model)
function isOpenCreditsModel(modelId) {
const claudeModels = ['opus', 'sonnet', 'default'];
return !claudeModels.includes(modelId);
}
// Render quick select buttons from recommended models data
function isModelMatch(modelId, candidate) {
if (modelId === candidate) return true;
// Check tier models
var m = openCreditsModels.find(function(om) { return om.id === candidate; });
if (m && m.tierModels) {
var match = m.tierModels.sonnet === modelId || m.tierModels.opus === modelId || m.tierModels.haiku === modelId;
return match;
}
return false;
}
function renderQuickButtons() {
const container = document.getElementById('modelQuickSelect');
if (!container) return;
const selectorRow = container.closest('.model-selector-row');
if (selectorRow) selectorRow.style.display = 'flex';
container.innerHTML = '';
var modelDropdown = document.getElementById('modelDropdownBtn');
var moreBtn = document.getElementById('modelMoreBtn');
if (!opencreditsEnabled && !hasOpenCreditsKey) {
// No OpenCredits - hide promo button, quick select, and more btn; show model dropdown
var mainBtn = document.getElementById('modelSelector');
if (mainBtn) mainBtn.style.display = 'none';
container.style.display = 'none';
if (moreBtn) moreBtn.style.display = 'none';
if (modelDropdown) modelDropdown.style.display = '';
return;
}
// OpenCredits enabled - show promo button, quick buttons, and more btn; hide model dropdown
var mainBtn = document.getElementById('modelSelector');
if (mainBtn) mainBtn.style.display = '';
container.style.display = '';
if (moreBtn) moreBtn.style.display = '';
if (modelDropdown) modelDropdown.style.display = 'none';
openCreditsModels.forEach(function(model) {
const btn = document.createElement('button');
btn.className = 'model-quick-btn' + (isModelMatch(currentModel, model.id) ? ' selected' : '');
btn.setAttribute('data-model', model.id);
btn.textContent = model.quickLabel || model.name;
btn.onclick = function() { selectModel(model.id); };
container.appendChild(btn);
});
}
renderQuickButtons();
function getCreditsPricing(modelId) {
var model = openCreditsModels.find(function(m) { return m.id === modelId; });
if (model && model.credits_per_request) {
return { id: model.id, credits_per_request: model.credits_per_request };
}
// Check live data for non-recommended models (e.g., reference model)
if (creditsPricingData && creditsPricingData.models) {
return creditsPricingData.models.find(function(m) { return m.id === modelId; }) || null;
}
return null;
}
function renderOpenCreditsModelCards() {
const container = document.getElementById('opencreditsModelCards');
if (!container) return;
// Comparison header
var headerHtml = '';
// Claude Opus 4.6 via Anthropic API directly: $5/MTok input, $25/MTok output
// At 2500 in + 2500 out = $0.075/request → $10 = ~133 requests
var OPUS_DIRECT_COST_PER_REQUEST = 0.075;
var OPUS_DIRECT_REQUESTS_PER_10 = Math.round(10 / OPUS_DIRECT_COST_PER_REQUEST); // 133
headerHtml = '<div class="model-comparison-header">' +
'Switch between any model with <strong>OpenCredits</strong>' +
'</div>';
// Sort by credits_per_request (cheapest first = highest savings)
var sortedModels = openCreditsModels.slice().sort(function(a, b) {
var aPricing = getCreditsPricing(a.id);
var bPricing = getCreditsPricing(b.id);
var aCost = aPricing ? aPricing.credits_per_request : 9999;
var bCost = bPricing ? bPricing.credits_per_request : 9999;
return aCost - bCost;
});
const modelCards = sortedModels.map(model => {
const isSelected = isModelMatch(currentModel, model.id);
const isPending = pendingModelSelection === model.id;
// Calculate savings vs Claude Opus direct (Anthropic API)
var badgeHtml = '';
var modelPricing = getCreditsPricing(model.id);
if (modelPricing && modelPricing.credits_per_request > 0) {
// Convert credits to dollars: credits × $0.008 (credit value after markup)
var modelCostPerRequest = modelPricing.credits_per_request * 0.008;
var savingsPercent = Math.round((1 - modelCostPerRequest / OPUS_DIRECT_COST_PER_REQUEST) * 100);
if (savingsPercent > 0) {
badgeHtml = '<div class="savings-badge">Save ' + savingsPercent + '%</div>';
}
}
return '<div class="model-card' + (isSelected ? ' selected' : '') + (isPending ? ' pending' : '') + '" data-model-id="' + model.id + '" data-provider="' + model.provider + '">' +
badgeHtml +
'<div class="model-card-provider">' + model.provider + '</div>' +
'<div class="model-card-name">' + model.name + '</div>' +
'</div>';
}).join('');
var browseCard = '<div class="model-card more-models-card" onclick="showAllModelsModal()">' +
'<div class="model-card-provider">Browse All OpenCredits</div>' +
'<div class="model-card-name">150+ Models</div>' +
'</div>';
var customCard;
if (hasOpenCreditsKey) {
customCard = '<div class="model-card more-models-card" onclick="showAdvancedModal()">' +
'<div class="model-card-provider">Advanced Settings</div>' +
'<div class="model-card-name">Configure Models</div>' +
'</div>';
} else {
customCard = '<div class="model-card more-models-card" onclick="showCustomProviderModal()">' +
'<div class="model-card-provider">Custom</div>' +
'<div class="model-card-name">Your Provider</div>' +
'</div>';
}
var headerContainer = document.getElementById('opencreditsComparisonHeader');
if (headerContainer) headerContainer.innerHTML = headerHtml;
container.innerHTML = modelCards + browseCard + customCard;
// Add click handlers
container.querySelectorAll('.model-card').forEach(card => {
card.addEventListener('click', () => {
const modelId = card.getAttribute('data-model-id');
if (modelId) {
if (hasOpenCreditsKey) {
selectModel(modelId);
} else {
triggerOpenCreditsCheckout(modelId);
}
}
});
});
}
function updateOpenCreditsPromo() {
const promo = document.getElementById('opencreditsPromo');
if (!promo) return;
// Hide promo entirely if OpenCredits is not enabled
if (!opencreditsEnabled && !hasOpenCreditsKey) {
promo.style.display = 'none';
return;
}
promo.style.display = '';
// Update envs label, toggle button, and list opacity
var envsLabel = document.getElementById('envsLabel');
var toggleBtn = document.getElementById('envsToggleBtn');
var envList = document.getElementById('env-variables-list');
var hasAnyEnvVars = document.querySelectorAll('.env-variable-row').length > 0;
if (envsLabel) {
if (envsDisabled) {
envsLabel.textContent = 'Environment Variables (Disabled)';
envsLabel.style.color = '#ef4444';
} else {
envsLabel.textContent = 'Environment Variables';
envsLabel.style.color = 'var(--vscode-descriptionForeground)';
}
}
if (toggleBtn) {
if (hasAnyEnvVars) {
toggleBtn.style.display = 'inline-block';
toggleBtn.textContent = envsDisabled ? 'Enable' : 'Disable';
toggleBtn.style.color = envsDisabled ? '#10b981' : 'var(--vscode-descriptionForeground)';
toggleBtn.onclick = function() {
envsDisabled = !envsDisabled;
vscode.postMessage({ type: 'setEnvsDisabled', disabled: envsDisabled });
if (!envsDisabled) {
// Re-enabling: let settingsData handler restore hasOpenCreditsKey
} else {
hasOpenCreditsKey = false;
openCreditsBalance = null;
updateStatusWithTotals();
}
updateOpenCreditsPromo();
};
} else {
toggleBtn.style.display = 'none';
}
}
if (envList) {
envList.style.opacity = envsDisabled ? '0.4' : '1';
envList.style.pointerEvents = envsDisabled ? 'none' : 'auto';
}
const btnStyle = 'padding: 5px 12px; background: rgba(139, 92, 246, 0.15); color: #a78bfa; border: 1px solid rgba(139, 92, 246, 0.3); border-radius: 6px; cursor: pointer; font-size: 11px; font-weight: 500; white-space: nowrap;';
const config = vscode.getState() || {};
// Check if env vars exist but are disabled
const hasEnvVars = !!(document.querySelectorAll('.env-variable-row .env-key').length > 0 ||
hasOpenCreditsKey);
if (hasOpenCreditsKey) {
const balanceLine = openCreditsBalance !== null ? '<div style="font-size: 11px; color: #10b981; margin-top: 3px;">' + openCreditsBalance.toFixed(1) + ' credits</div>' : '';
promo.innerHTML =
'<div style="display: flex; align-items: center; justify-content: space-between;">' +
'<div>' +
'<div style="font-size: 12px; font-weight: 600; color: var(--vscode-foreground);">OpenCredits Connected</div>' +
'<div style="font-size: 11px; color: var(--vscode-descriptionForeground); margin-top: 2px;">Access 150+ models via OpenCredits</div>' +
balanceLine +
'</div>' +
'<button id="opencreditsPromoBtn" style="' + btnStyle + '">My Account</button>' +
'</div>';
document.getElementById('opencreditsPromoBtn').onclick = function() {
OpenCredits.open();
};
} else if (hasSavedOpenCreditsKey) {
promo.innerHTML =
'<div style="display: flex; align-items: center; justify-content: space-between;">' +
'<div>' +
'<div style="font-size: 12px; font-weight: 600; color: var(--vscode-foreground);">OpenCredits Account Found</div>' +
'<div style="font-size: 11px; color: var(--vscode-descriptionForeground); margin-top: 2px;">Your account is saved. Reconnect to use 150+ models.</div>' +
'</div>' +
'<button id="opencreditsPromoBtn" style="' + btnStyle + '">Reconnect</button>' +
'</div>';
document.getElementById('opencreditsPromoBtn').onclick = function() {
vscode.postMessage({ type: 'restoreOpenCredits' });
};
} else {
promo.innerHTML =
'<div style="display: flex; align-items: center; justify-content: space-between;">' +
'<div>' +
'<div style="font-size: 12px; font-weight: 600; color: var(--vscode-foreground);">Use 100+ AI Models</div>' +
'<div style="font-size: 11px; color: var(--vscode-descriptionForeground); margin-top: 2px;">Access GPT, Gemini, DeepSeek & more via OpenCredits. Pay-as-you-go.</div>' +
'</div>' +
'<button id="opencreditsPromoBtn" style="' + btnStyle + '">Browse Models</button>' +
'</div>';
document.getElementById('opencreditsPromoBtn').onclick = function() {
hideSettingsModal();
showModelSelector();
};
}
}
// Trigger checkout flow for OpenCredits
function triggerOpenCreditsCheckout(modelId) {
sendStats('Checkout started', { model: modelId });
pendingModelSelection = modelId;
hideModelModal();
// Tell extension about pending model
if (pendingModelSelection) {
vscode.postMessage({
type: 'setPendingModel',
pendingModel: pendingModelSelection
});
}
// Open OpenCredits checkout directly
OpenCredits.open({ model: modelId });
}
function showModelSelector(fromModel) {
sendStats('Model selector opened', fromModel ? { model: fromModel } : undefined);
document.getElementById('modelModal').style.display = 'flex';
const opencreditsSection = document.getElementById('opencreditsModelsSection');
const claudeSection = document.getElementById('claudeCodeSection');
const divider = document.getElementById('modelSectionDivider');
// Always show Claude Code section
claudeSection.style.display = 'block';
// Show OpenCredits section only if feature flag enabled
if (opencreditsEnabled || hasOpenCreditsKey) {
opencreditsSection.style.display = 'block';
divider.style.display = 'block';
} else {
opencreditsSection.style.display = 'none';
divider.style.display = 'none';
}
renderOpenCreditsModelCards();
// Update selected state
updateModelSelection();
}
function updateModelSelection() {
// Update Claude card selection
document.querySelectorAll('.claude-card').forEach(card => {
card.classList.toggle('selected', card.getAttribute('data-model') === currentModel);
});
// Update OpenCredits card selection
document.querySelectorAll('.model-card').forEach(card => {
card.classList.toggle('selected', card.getAttribute('data-model-id') === currentModel);
});
}
function hideModelModal() {
document.getElementById('modelModal').style.display = 'none';
}
// All models browser
let allModelsCache = null;
function showCustomProviderModal() {
hideModelModal();
document.getElementById('customProviderModal').style.display = 'flex';
}
function hideCustomProviderModal() {
document.getElementById('customProviderModal').style.display = 'none';
}
function saveCustomProvider() {
var baseUrl = document.getElementById('customProviderBaseUrl').value.trim();
var authToken = document.getElementById('customProviderAuthToken').value.trim();
var sonnetModel = document.getElementById('customProviderSonnet').value.trim();
var opusModel = document.getElementById('customProviderOpus').value.trim();
var haikuModel = document.getElementById('customProviderHaiku').value.trim();
if (!baseUrl || !authToken) {
return;
}
var envVars = {
'ANTHROPIC_BASE_URL': baseUrl,
'ANTHROPIC_AUTH_TOKEN': authToken
};
if (sonnetModel) envVars['ANTHROPIC_DEFAULT_SONNET_MODEL'] = sonnetModel;
if (opusModel) envVars['ANTHROPIC_DEFAULT_OPUS_MODEL'] = opusModel;
if (haikuModel) envVars['ANTHROPIC_DEFAULT_HAIKU_MODEL'] = haikuModel;
vscode.postMessage({
type: 'saveCustomProvider',
envVars: envVars
});
hideCustomProviderModal();
}
// Model combo box component
function initModelCombo(comboId) {
var combo = document.getElementById(comboId);
if (!combo) return;
var input = combo.querySelector('.model-combo-input');
var dropdown = combo.querySelector('.model-combo-dropdown');
function renderDropdown(query) {
var models = allModelsCache || [];
var q = (query || '').toLowerCase();
var filtered = q ? models.filter(function(m) {
return m.id.toLowerCase().indexOf(q) !== -1 || (m.name || '').toLowerCase().indexOf(q) !== -1;
}) : models;
var html = filtered.slice(0, 50).map(function(m) {
return '<div class="model-combo-option" data-id="' + m.id + '">' +
'<div class="model-combo-option-name">' + (m.name || m.id) + '</div>' +
'<div class="model-combo-option-id">' + m.id + '</div>' +
'</div>';
}).join('');
if (q && filtered.length === 0) {
html = '<div class="model-combo-custom" data-id="' + q + '">Use "' + q + '" as custom model</div>';
} else if (q && !filtered.find(function(m) { return m.id === q; })) {
html += '<div class="model-combo-custom" data-id="' + q + '">Use "' + q + '" as custom model</div>';
}
dropdown.innerHTML = html;
dropdown.querySelectorAll('.model-combo-option, .model-combo-custom').forEach(function(opt) {
opt.addEventListener('mousedown', function(e) {
e.preventDefault();
input.value = opt.getAttribute('data-id');
combo.classList.remove('open');
});
});
}
input.addEventListener('focus', function() {
// Clear to show all models, store current value
input._prevValue = input.value;
input.value = '';
combo.classList.add('open');
renderDropdown('');
});
input.addEventListener('input', function() {
combo.classList.add('open');
renderDropdown(input.value);
});
input.addEventListener('blur', function() {
setTimeout(function() {
combo.classList.remove('open');
// Restore previous value if left empty
if (!input.value && input._prevValue) {
input.value = input._prevValue;
}
}, 150);
});
combo.setValue = function(id) { input.value = id || ''; };
combo.getValue = function() { return input.value.trim(); };
}
var comboSonnet, comboOpus, comboHaiku;
async function showAdvancedModal() {
hideModelModal();
document.getElementById('advancedModal').style.display = 'flex';
// Fetch models if not cached
if (!allModelsCache) {
try {
const response = await fetch(OPENCREDITS_API_URL + '/v1/models');
const data = await response.json();
allModelsCache = data.data || data;
} catch (e) { /* ignore */ }
}
// Init combos
if (!comboSonnet) {
initModelCombo('comboSonnet');
initModelCombo('comboOpus');
initModelCombo('comboHaiku');
comboSonnet = document.getElementById('comboSonnet');
comboOpus = document.getElementById('comboOpus');
comboHaiku = document.getElementById('comboHaiku');
}
// Request current env vars to populate
vscode.postMessage({ type: 'getEnvVars' });
}
function hideAdvancedModal() {
document.getElementById('advancedModal').style.display = 'none';
}
function saveAdvancedSettings() {
var sonnetModel = comboSonnet ? comboSonnet.getValue() : '';
var opusModel = comboOpus ? comboOpus.getValue() : '';
var haikuModel = comboHaiku ? comboHaiku.getValue() : '';
var envVars = {};
envVars['ANTHROPIC_DEFAULT_SONNET_MODEL'] = sonnetModel;
envVars['ANTHROPIC_DEFAULT_OPUS_MODEL'] = opusModel;
envVars['ANTHROPIC_DEFAULT_HAIKU_MODEL'] = haikuModel;
vscode.postMessage({
type: 'saveCustomProvider',
envVars: envVars
});
hideAdvancedModal();
}
async function showAllModelsModal() {
const modal = document.getElementById('allModelsModal');
const listContainer = document.getElementById('allModelsList');
const searchInput = document.getElementById('allModelsSearch');
modal.style.display = 'flex';
hideModelModal();
// Show loading state
listContainer.innerHTML = '<div class="all-models-loading">Loading models...</div>';
// Fetch models if not cached
if (!allModelsCache) {
try {
const response = await fetch(OPENCREDITS_API_URL + '/v1/models');
const data = await response.json();
allModelsCache = data.data || data;
} catch (error) {
listContainer.innerHTML = '<div class="all-models-error">Failed to load models. Please try again.</div>';
return;
}
}
renderAllModels(allModelsCache);
searchInput.value = '';
searchInput.focus();
}
function hideAllModelsModal() {
document.getElementById('allModelsModal').style.display = 'none';
}
function renderAllModels(models) {
const listContainer = document.getElementById('allModelsList');
if (!models || models.length === 0) {
listContainer.innerHTML = '<div class="all-models-empty">No models found</div>';
return;
}
listContainer.innerHTML = models.map(model => {
const isSelected = currentModel === model.id;
const contextLength = model.context_length ? Math.round(model.context_length / 1000) + 'K' : '';
return '<div class="all-models-item' + (isSelected ? ' selected' : '') + '" data-model-id="' + model.id + '">' +
'<div class="all-models-item-main">' +
'<div class="all-models-item-name">' + (model.name || model.id) + '</div>' +
'<div class="all-models-item-provider">' + (model.owned_by || '') + '</div>' +
'</div>' +
(contextLength ? '<div class="all-models-item-details"><span class="all-models-item-context">' + contextLength + '</span></div>' : '') +
'</div>';
}).join('');
// Add click handlers
listContainer.querySelectorAll('.all-models-item').forEach(item => {
item.addEventListener('click', () => {
const modelId = item.getAttribute('data-model-id');
if (modelId) {
if (hasOpenCreditsKey) {
selectModel(modelId);
hideAllModelsModal();
} else {
triggerOpenCreditsCheckout(modelId);
hideAllModelsModal();
}
}
});
});
}
function filterAllModels() {
const searchInput = document.getElementById('allModelsSearch');
const query = searchInput.value.toLowerCase();
if (!allModelsCache) return;
const filtered = allModelsCache.filter(model => {
const name = (model.name || model.id || '').toLowerCase();
const provider = (model.owned_by || '').toLowerCase();
return name.includes(query) || provider.includes(query);
});
renderAllModels(filtered);
}
// Helper function to get display name for a model
function getModelDisplayName(modelId) {
const claudeModels = {
'opus': 'Claude Opus',
'sonnet': 'Claude Sonnet',
'default': 'Claude'
};
if (claudeModels[modelId]) return claudeModels[modelId];
// Check OpenCredits models
const openCreditsModel = openCreditsModels.find(m => m.id === modelId);
if (openCreditsModel) return openCreditsModel.name;
// Generate display name from model ID
// Remove provider prefix (e.g., "deepseek/", "z-ai/")
let name = modelId.includes('/') ? modelId.split('/').pop() : modelId;
// Remove suffixes like ":thinking", ":free"
name = name.split(':')[0];
// Replace hyphens/underscores with spaces and capitalize words
name = name.replace(/[-_]/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase());
return name;
}
// Update UI to reflect current model selection
function updateModelSelectionUI() {
// Update Claude cards
document.querySelectorAll('.claude-card').forEach(card => {
card.classList.toggle('selected', card.getAttribute('data-model') === currentModel);
});
// Update OpenCredits cards
document.querySelectorAll('.model-card').forEach(card => {
card.classList.toggle('selected', card.getAttribute('data-model-id') === currentModel);
});
// Update quick select buttons
document.querySelectorAll('.model-quick-btn').forEach(btn => {
btn.classList.toggle('selected', btn.getAttribute('data-model') === currentModel);
});
// Update current model indicator and selector button
updateCurrentModelDisplay();
}
function updateCurrentModelDisplay() {
const selectorText = document.getElementById('modelSelectorText');
const selectorBadge = document.getElementById('modelSelectorBadge');
const modelDropdown = document.getElementById('modelDropdownText');
const predefinedModels = openCreditsModels.map(function(m) { return m.id; });
// Update inline model dropdown
if (modelDropdown) {
if (currentModel === 'opus') {
modelDropdown.textContent = 'Opus';
} else if (currentModel === 'sonnet') {
modelDropdown.textContent = 'Sonnet';
} else if (currentModel === 'default') {
modelDropdown.textContent = 'Model';
} else {
var displayName = getModelDisplayName(currentModel);
var words = displayName.split(' ');
modelDropdown.textContent = words.slice(0, 2).join(' ');
}
}
if (currentModel === 'opus' || currentModel === 'sonnet') {
// Claude model selected - show model name, hide badge
const modelName = currentModel === 'opus' ? 'Claude Opus' : 'Claude Sonnet';
selectorText.textContent = modelName;
selectorBadge.style.display = 'none';
} else if (currentModel === 'default' || predefinedModels.includes(currentModel)) {
// Default or predefined OpenCredits model - show "Try other models" with NEW badge
selectorText.textContent = 'Try other models';
selectorBadge.style.display = '';
} else {
// Other OpenCredits model - show in button, hide badge
const displayName = getModelDisplayName(currentModel);
const words = displayName.split(' ');
const shortName = words.slice(0, 2).join(' ');
selectorText.textContent = shortName;
selectorBadge.style.display = 'none';
}
}
// Show toast notification
function showToast(message) {
// Remove any existing toast
const existingToast = document.querySelector('.toast-notification');
if (existingToast) existingToast.remove();
const toast = document.createElement('div');
toast.className = 'toast-notification';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => toast.remove(), 300);
}, 2500);
}
// Slash commands modal functions
function showSlashCommandsModal() {
document.getElementById('slashCommandsModal').style.display = 'flex';
// Auto-focus the search input
setTimeout(() => {
document.getElementById('slashCommandsSearch').focus();
}, 100);
}
function hideSlashCommandsModal() {
document.getElementById('slashCommandsModal').style.display = 'none';
}
// Install modal functions
function showInstallModal() {
const modal = document.getElementById('installModal');
const main = document.getElementById('installMain');
const progress = document.getElementById('installProgress');
const success = document.getElementById('installSuccess');
const checkout = document.getElementById('installCheckout');
if (modal) modal.style.display = 'flex';
if (main) main.style.display = 'flex';
if (progress) progress.style.display = 'none';
if (success) success.style.display = 'none';
if (checkout) checkout.style.display = 'none';
sendStats('Install modal shown');
}
function showLoginOptionsModal() {
var modal = document.getElementById('installModal');
var main = document.getElementById('installMain');
var progress = document.getElementById('installProgress');
var success = document.getElementById('installSuccess');
var checkout = document.getElementById('installCheckout');
var funds = document.getElementById('installFunds');
// Update text for login flow
var successIcon = success ? success.querySelector('.install-success-icon') : null;
var successText = success ? success.querySelector('.install-success-text') : null;
var successHint = success ? success.querySelector('.install-success-hint') : null;
if (successIcon) successIcon.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
if (successText) successText.textContent = 'Login Required';
if (successHint) successHint.textContent = 'How would you like to use Claude Code?';
if (modal) modal.style.display = 'flex';
if (main) main.style.display = 'none';
if (progress) progress.style.display = 'none';
if (success) success.style.display = 'flex';
if (checkout) checkout.style.display = 'none';
if (funds) funds.style.display = 'none';
// Hide OpenCredits option if feature flag is disabled
var ocOption = document.getElementById('installOpenCreditsOption');
if (ocOption) ocOption.style.display = opencreditsEnabled ? '' : 'none';
sendStats('Login options shown');
}
function hideInstallModal() {
document.getElementById('installModal').style.display = 'none';
// Clean up checkout iframe and timeout
var iframe = document.getElementById('checkoutIframe');
if (iframe) iframe.src = '';
if (checkoutTimeoutId) {
clearTimeout(checkoutTimeoutId);
checkoutTimeoutId = null;
}
}
function startInstallation() {
sendStats('Install started');
// Hide main content, show progress
document.getElementById('installMain').style.display = 'none';
document.getElementById('installProgress').style.display = 'flex';
// Extension handles platform detection and command selection
vscode.postMessage({
type: 'runInstallCommand'
});
}
function handleInstallComplete(success, error) {
document.getElementById('installProgress').style.display = 'none';
const successEl = document.getElementById('installSuccess');
successEl.style.display = 'flex';
// Hide OpenCredits option if feature flag is disabled
const ocOption = document.getElementById('installOpenCreditsOption');
if (ocOption) ocOption.style.display = opencreditsEnabled ? '' : 'none';
if (success) {
sendStats('Install success');
successEl.querySelector('.install-success-text').textContent = 'Installed';
successEl.querySelector('.install-success-hint').textContent = 'Send a message to get started';
} else {
sendStats('Install failed', { error: (error || 'Unknown error').substring(0, 200) });
// Show error state
successEl.querySelector('.install-success-icon').style.display = 'none';
successEl.querySelector('.install-success-text').textContent = 'Installation failed';
successEl.querySelector('.install-success-hint').textContent = error || 'Try installing manually from claude.ai/download';
successEl.querySelector('.install-options').style.display = 'none';
}
}
function loginWithPlan() {
hideInstallModal();
vscode.postMessage({ type: 'openLoginTerminal' });
}
function showFundsSelection() {
// Tell extension about pending model
if (pendingModelSelection) {
vscode.postMessage({
type: 'setPendingModel',
pendingModel: pendingModelSelection
});
}
// Skip the custom amount picker — open OpenCredits checkout directly
hideInstallModal();
OpenCredits.open();
}
function showInstallOptions() {
document.getElementById('installFunds').style.display = 'none';
document.getElementById('installSuccess').style.display = 'flex';
}
var checkoutTimeoutId = null;
var checkoutAmount = null;
function showCheckoutError(msg) {
document.getElementById('checkoutPreparing').style.display = 'none';
document.getElementById('checkoutReady').style.display = 'none';
document.getElementById('checkoutComplete').style.display = 'none';
var errorEl = document.getElementById('checkoutError');
errorEl.style.display = 'block';
if (msg) {
document.getElementById('checkoutErrorMsg').textContent = msg;
}
// Wire retry button to restart with same amount
document.getElementById('checkoutRetryBtn').onclick = function() {
if (checkoutAmount) {
startCheckoutFlow(checkoutAmount);
} else {
showFundsSelection();
}
};
// Clear timeout if any
if (checkoutTimeoutId) {
clearTimeout(checkoutTimeoutId);
checkoutTimeoutId = null;
}
}
function startCheckoutFlow(amount) {
checkoutAmount = amount;
// Tell extension about pending model
if (pendingModelSelection) {
vscode.postMessage({
type: 'setPendingModel',
pendingModel: pendingModelSelection
});
}
// Hide the install modal — the SDK overlay handles everything
hideInstallModal();
// Open the OpenCredits SDK checkout overlay
OpenCredits.open({ amount: amount || undefined });
}
function selectFundsAmount(amount) {
startCheckoutFlow(amount);
}
function selectCustomAmount() {
var input = document.getElementById('customAmountInput');
var amount = parseInt(input.value);
if (amount && amount >= 1 && amount <= 500) {
startCheckoutFlow(amount);
}
}
// OpenCredits checkout is handled by the SDK (inlined at top of script).
// Callbacks are wired in OpenCredits.init() above.
// Thinking intensity modal functions
function showThinkingIntensityModal() {
// Request current settings from VS Code first
vscode.postMessage({
type: 'getSettings'
});
document.getElementById('thinkingIntensityModal').style.display = 'flex';
}
function hideThinkingIntensityModal() {
document.getElementById('thinkingIntensityModal').style.display = 'none';
}
function saveThinkingIntensity() {
const thinkingSlider = document.getElementById('thinkingIntensitySlider');
const intensityValues = ['think', 'think-hard', 'think-harder', 'ultrathink'];
const thinkingIntensity = intensityValues[thinkingSlider.value] || 'think';
// Send settings to VS Code
vscode.postMessage({
type: 'updateSettings',
settings: {
'thinking.intensity': thinkingIntensity
}
});
}
function updateThinkingModeToggleName(intensityValue) {
var intensityNames = ['Think', 'Think Hard', 'Think Harder', 'Ultrathink'];
var modeName = intensityNames[intensityValue] || 'Think';
var toggleLabel = document.getElementById('thinkingModeLabel');
if (toggleLabel) {
toggleLabel.textContent = modeName + ' Mode';
}
var thinkBtn = document.getElementById('thinkToggleBtn');
if (thinkBtn) {
thinkBtn.textContent = modeName;
}
}
function updateThinkingIntensityDisplay(value) {
// Update label highlighting for thinking intensity modal
for (let i = 0; i < 4; i++) {
const label = document.getElementById('thinking-label-' + i);
if (i == value) {
label.classList.add('active');
} else {
label.classList.remove('active');
}
}
// Don't update toggle name until user confirms
}
function setThinkingIntensityValue(value) {
// Set slider value for thinking intensity modal
document.getElementById('thinkingIntensitySlider').value = value;
// Update visual state
updateThinkingIntensityDisplay(value);
}
function confirmThinkingIntensity() {
// Get the current slider value
const currentValue = document.getElementById('thinkingIntensitySlider').value;
// Update the toggle name with confirmed selection
updateThinkingModeToggleName(currentValue);
// Save the current intensity setting
saveThinkingIntensity();
// Close the modal
hideThinkingIntensityModal();
}
// WSL Alert functions
function showWSLAlert() {
const alert = document.getElementById('wslAlert');
if (alert) {
alert.style.display = 'block';
}
}
function dismissWSLAlert() {
const alert = document.getElementById('wslAlert');
if (alert) {
alert.style.display = 'none';
}
// Send dismiss message to extension to store in globalState
vscode.postMessage({
type: 'dismissWSLAlert'
});
}
function openWSLSettings() {
// Dismiss the alert
dismissWSLAlert();
// Open settings modal
toggleSettings();
}
function executeSlashCommand(command) {
// Hide the modal
hideSlashCommandsModal();
// Clear the input since user selected a command
messageInput.value = '';
// Send command to VS Code to execute
vscode.postMessage({
type: 'executeSlashCommand',
command: command
});
// Show user feedback - /compact runs in chat, others in terminal
if (command === 'compact') {
// No message needed - compact runs in chat and shows its own status
} else {
addMessage('user', \`Executing /\${command} command in terminal. Check the terminal output and return when ready.\`, 'assistant');
}
}
function handleCustomCommandKeydown(event) {
if (event.key === 'Enter') {
event.preventDefault();
const customCommand = event.target.value.trim();
if (customCommand) {
executeSlashCommand(customCommand);
// Clear the input for next use
event.target.value = '';
}
}
}
// Store custom snippets data globally
let customSnippetsData = {};
function usePromptSnippet(snippetType) {
const builtInSnippets = {
'performance-analysis': 'Analyze this code for performance issues and suggest optimizations',
'security-review': 'Review this code for security vulnerabilities',
'implementation-review': 'Review the implementation in this code',
'code-explanation': 'Explain how this code works in detail',
'bug-fix': 'Help me fix this bug in my code',
'refactor': 'Refactor this code to improve readability and maintainability',
'test-generation': 'Generate comprehensive tests for this code',
'documentation': 'Generate documentation for this code'
};
// Check built-in snippets first
let promptText = builtInSnippets[snippetType];
// If not found in built-in, check custom snippets
if (!promptText && customSnippetsData[snippetType]) {
promptText = customSnippetsData[snippetType].prompt;
}
if (promptText) {
// Hide the modal
hideSlashCommandsModal();
// Insert the prompt into the message input
messageInput.value = promptText;
messageInput.focus();
// Auto-resize the textarea
autoResizeTextarea();
}
}
function showAddSnippetForm() {
document.getElementById('addSnippetForm').style.display = 'block';
document.getElementById('snippetName').focus();
}
function hideAddSnippetForm() {
document.getElementById('addSnippetForm').style.display = 'none';
// Clear form fields
document.getElementById('snippetName').value = '';
document.getElementById('snippetPrompt').value = '';
}
function saveCustomSnippet() {
const name = document.getElementById('snippetName').value.trim();
const prompt = document.getElementById('snippetPrompt').value.trim();
if (!name || !prompt) {
alert('Please fill in both name and prompt text.');
return;
}
// Generate a unique ID for the snippet
const snippetId = 'custom-' + Date.now();
// Save the snippet using VS Code global storage
const snippetData = {
name: name,
prompt: prompt,
id: snippetId
};
vscode.postMessage({
type: 'saveCustomSnippet',
snippet: snippetData
});
// Hide the form
hideAddSnippetForm();
}
function loadCustomSnippets(snippetsData = {}) {
const snippetsList = document.getElementById('promptSnippetsList');
// Remove existing custom snippets
const existingCustom = snippetsList.querySelectorAll('.custom-snippet-item');
existingCustom.forEach(item => item.remove());
// Add custom snippets after the add button and form
const addForm = document.getElementById('addSnippetForm');
Object.values(snippetsData).forEach(snippet => {
const snippetElement = document.createElement('div');
snippetElement.className = 'slash-command-item prompt-snippet-item custom-snippet-item';
snippetElement.onclick = () => usePromptSnippet(snippet.id);
snippetElement.innerHTML = \`
<div class="slash-command-icon">📝</div>
<div class="slash-command-content">
<div class="slash-command-title">/\${snippet.name}</div>
<div class="slash-command-description">\${snippet.prompt}</div>
</div>
<div class="snippet-actions">
<button class="snippet-delete-btn" onclick="event.stopPropagation(); deleteCustomSnippet('\${snippet.id}')" title="Delete snippet">🗑️</button>
</div>
\`;
// Insert after the form
addForm.parentNode.insertBefore(snippetElement, addForm.nextSibling);
});
}
function deleteCustomSnippet(snippetId) {
vscode.postMessage({
type: 'deleteCustomSnippet',
snippetId: snippetId
});
}
function filterSlashCommands() {
const searchTerm = document.getElementById('slashCommandsSearch').value.toLowerCase();
const allItems = document.querySelectorAll('.slash-command-item');
allItems.forEach(item => {
const title = item.querySelector('.slash-command-title').textContent.toLowerCase();
const description = item.querySelector('.slash-command-description').textContent.toLowerCase();
if (title.includes(searchTerm) || description.includes(searchTerm)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
}
});
}
function openModelTerminal() {
vscode.postMessage({
type: 'openModelTerminal'
});
hideModelModal();
}
function showProviderChoice(model) {
hideModelModal();
var modal = document.getElementById('providerChoiceModal');
var modelName = model.charAt(0).toUpperCase() + model.slice(1);
document.getElementById('providerChoiceTitle').textContent = 'Use ' + modelName + ' via';
modal.style.display = 'flex';
document.getElementById('providerChoiceOpenCredits').onclick = function() {
modal.style.display = 'none';
// Re-enable envs if they were disabled
if (envsDisabled) {
envsDisabled = false;
vscode.postMessage({ type: 'setEnvsDisabled', disabled: false });
}
selectModel(model, true);
var modelInfo = openCreditsModels.find(function(m) { return m.id === model; });
var msg = { type: 'selectModel', model: model };
if (modelInfo && modelInfo.tierModels) { msg.tierModels = modelInfo.tierModels; }
vscode.postMessage(msg);
};
document.getElementById('providerChoiceAnthropic').onclick = function() {
modal.style.display = 'none';
// Disable envs so Claude CLI uses Anthropic directly
envsDisabled = true;
hasOpenCreditsKey = false;
openCreditsBalance = null;
vscode.postMessage({ type: 'setEnvsDisabled', disabled: true });
selectModel(model, true);
vscode.postMessage({ type: 'selectModel', model: model });
updateStatusWithTotals();
updateOpenCreditsPromo();
};
}
function selectModel(model, fromBackend = false) {
// If clicking on already selected model, show modal
if (model === currentModel && !fromBackend) {
showModelSelector(model);
return;
}
// Check if this is a OpenCredits model (not a standard Claude model)
const claudeModels = ['opus', 'sonnet', 'default'];
const isOpenCreditsModel = !claudeModels.includes(model);
// If selecting a OpenCredits model and envs are disabled, re-enable them
if (isOpenCreditsModel && envsDisabled && !fromBackend) {
envsDisabled = false;
hasOpenCreditsKey = true;
vscode.postMessage({ type: 'setEnvsDisabled', disabled: false });
updateStatusWithTotals();
updateOpenCreditsPromo();
// Fall through to select the model normally
}
// If selecting a OpenCredits model without a key, trigger checkout flow
if (isOpenCreditsModel && !hasOpenCreditsKey && !fromBackend) {
pendingModelSelection = model;
showModelSelector(model);
setTimeout(() => {
const opencreditsSection = document.querySelector('.model-modal-section:last-child');
if (opencreditsSection) {
opencreditsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
return;
}
// If selecting a Claude model and user has OpenCredits configured, ask which provider
if (!isOpenCreditsModel && hasOpenCreditsKey && !fromBackend && !envsDisabled) {
showProviderChoice(model);
return;
}
currentModel = model;
if (!fromBackend) {
sendStats('Model selected', { model: model });
}
// Only send model selection to VS Code extension if not from backend
if (!fromBackend) {
// Look up tier-specific models if available
const modelInfo = openCreditsModels.find(m => m.id === model);
const msg = { type: 'selectModel', model: model };
if (modelInfo && modelInfo.tierModels) {
msg.tierModels = modelInfo.tierModels;
}
vscode.postMessage(msg);
}
// Update card selection states
// Claude cards
document.querySelectorAll('.claude-card').forEach(card => {
card.classList.toggle('selected', card.getAttribute('data-model') === model);
});
// OpenCredits cards
document.querySelectorAll('.model-card').forEach(card => {
card.classList.toggle('selected', card.getAttribute('data-model-id') === model);
});
// Quick select buttons
document.querySelectorAll('.model-quick-btn').forEach(btn => {
btn.classList.toggle('selected', btn.getAttribute('data-model') === model);
});
// Update the model selector button text
updateCurrentModelDisplay();
hideModelModal();
}
// Initialize model
currentModel = 'opus';
// Set initial quick button selected state
document.querySelectorAll('.model-quick-btn').forEach(btn => {
btn.classList.toggle('selected', btn.getAttribute('data-model') === currentModel);
});
// Close model modal when clicking outside
document.getElementById('modelModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('modelModal')) {
hideModelModal();
}
});
// Stop button functions
function showStopButton() {
document.getElementById('sendBtn').style.display = 'none';
document.getElementById('stopInlineBtn').style.display = 'inline-flex';
}
function hideStopButton() {
document.getElementById('sendBtn').style.display = '';
document.getElementById('stopInlineBtn').style.display = 'none';
}
function stopRequest() {
sendStats('Stop request');
vscode.postMessage({
type: 'stopRequest'
});
hideStopButton();
}
// Disable/enable buttons during processing
function disableButtons() {
const sendBtn = document.getElementById('sendBtn');
if (sendBtn) sendBtn.disabled = true;
}
function enableButtons() {
const sendBtn = document.getElementById('sendBtn');
if (sendBtn) sendBtn.disabled = false;
}
// Copy message content function
function copyMessageContent(messageDiv) {
const contentDiv = messageDiv.querySelector('.message-content');
if (contentDiv) {
// Get text content, preserving line breaks
const text = contentDiv.innerText || contentDiv.textContent;
// Copy to clipboard
navigator.clipboard.writeText(text).then(() => {
// Show brief feedback
const copyBtn = messageDiv.querySelector('.copy-btn');
const originalHtml = copyBtn.innerHTML;
copyBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>';
copyBtn.style.color = '#4caf50';
setTimeout(() => {
copyBtn.innerHTML = originalHtml;
copyBtn.style.color = '';
}, 1000);
}).catch(err => {
console.error('Failed to copy message:', err);
});
}
}
function copyCodeBlock(codeId) {
const codeElement = document.getElementById(codeId);
if (codeElement) {
const rawCode = codeElement.getAttribute('data-raw-code');
if (rawCode) {
// Decode HTML entities
const decodedCode = rawCode.replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
navigator.clipboard.writeText(decodedCode).then(() => {
// Show temporary feedback
const copyBtn = codeElement.closest('.code-block-container').querySelector('.code-copy-btn');
if (copyBtn) {
const originalInnerHTML = copyBtn.innerHTML;
copyBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>';
copyBtn.style.color = '#4caf50';
setTimeout(() => {
copyBtn.innerHTML = originalInnerHTML;
copyBtn.style.color = '';
}, 1000);
}
}).catch(err => {
console.error('Failed to copy code:', err);
});
}
}
}
window.addEventListener('message', event => {
const message = event.data;
switch (message.type) {
case 'ready':
addMessage(message.data, 'system');
updateStatusWithTotals();
break;
case 'restoreInputText':
const inputField = document.getElementById('messageInput');
if (inputField && message.data) {
inputField.value = message.data;
// Auto-resize the textarea
inputField.style.height = 'auto';
inputField.style.height = Math.min(inputField.scrollHeight, 200) + 'px';
}
break;
case 'output':
if (message.data.trim()) {
let displayData = message.data;
// Check if this is a usage limit message with Unix timestamp
const usageLimitMatch = displayData.match(/Claude AI usage limit reached\\|(\\d+)/);
if (usageLimitMatch) {
const timestamp = parseInt(usageLimitMatch[1]);
const date = new Date(timestamp * 1000);
const readableDate = date.toLocaleString(
undefined,
{
weekday: 'short',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true,
timeZoneName: 'short',
year: 'numeric'
}
);
displayData = displayData.replace(usageLimitMatch[0], \`Claude AI usage limit reached: \${readableDate}\`);
}
addMessage(parseSimpleMarkdown(displayData), 'claude');
}
updateStatusWithTotals();
break;
case 'userInput':
if (message.data.trim()) {
addMessage(parseSimpleMarkdown(message.data), 'user');
}
break;
case 'loading':
addMessage(message.data, 'system');
updateStatusWithTotals();
break;
case 'setProcessing':
isProcessing = message.data.isProcessing;
if (isProcessing) {
startRequestTimer(message.data.requestStartTime);
showStopButton();
disableButtons();
showProcessingIndicator();
} else {
stopRequestTimer();
hideStopButton();
enableButtons();
hideProcessingIndicator();
}
updateStatusWithTotals();
break;
case 'clearLoading':
// Remove the last loading message
const messages = messagesDiv.children;
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
if (lastMessage.classList.contains('system')) {
lastMessage.remove();
}
}
updateStatusWithTotals();
break;
case 'error':
if (message.data.trim()) {
// Check if this is an install required error
if (message.data.includes('Install claude code first') ||
message.data.includes('command not found') ||
message.data.includes('ENOENT')) {
sendStats('Install required');
}
addMessage(message.data, 'error');
}
updateStatusWithTotals();
break;
case 'toolUse':
if (typeof message.data === 'object') {
addToolUseMessage(message.data);
} else if (message.data.trim()) {
addMessage(message.data, 'tool');
}
break;
case 'toolResult':
addToolResultMessage(message.data);
break;
case 'thinking':
if (message.data.trim()) {
addMessage('💭 Thinking...' + parseSimpleMarkdown(message.data), 'thinking');
}
break;
case 'sessionInfo':
if (message.data.sessionId) {
showSessionInfo(message.data.sessionId);
// Show detailed session information
const sessionDetails = [
\`🆔 Session ID: \${message.data.sessionId}\`,
\`🔧 Tools Available: \${message.data.tools.length}\`,
\`🖥️ MCP Servers: \${message.data.mcpServers ? message.data.mcpServers.length : 0}\`
];
//addMessage(sessionDetails.join('\\n'), 'system');
}
break;
case 'imageAttached':
if (message.filePath && message.previewUri) {
addAttachedImage(message.filePath, message.previewUri);
}
break;
case 'updateTokens':
// Update token totals in real-time
totalTokensInput = message.data.totalTokensInput || 0;
totalTokensOutput = message.data.totalTokensOutput || 0;
// Update status bar immediately
updateStatusWithTotals();
// Show detailed token breakdown for current message
const currentTotal = (message.data.currentInputTokens || 0) + (message.data.currentOutputTokens || 0);
if (currentTotal > 0) {
let tokenBreakdown = \`📊 Tokens: \${currentTotal.toLocaleString()}\`;
if (message.data.cacheCreationTokens || message.data.cacheReadTokens) {
const cacheInfo = [];
if (message.data.cacheCreationTokens) cacheInfo.push(\`\${message.data.cacheCreationTokens.toLocaleString()} cache created\`);
if (message.data.cacheReadTokens) cacheInfo.push(\`\${message.data.cacheReadTokens.toLocaleString()} cache read\`);
tokenBreakdown += \`\${cacheInfo.join(' • ')}\`;
}
addMessage(tokenBreakdown, 'system');
}
break;
case 'updateTotals':
// Update local tracking variables
totalCost = message.data.totalCost || 0;
totalTokensInput = message.data.totalTokensInput || 0;
totalTokensOutput = message.data.totalTokensOutput || 0;
requestCount = message.data.requestCount || 0;
// Update status bar with new totals
updateStatusWithTotals();
// Show current request info if available (only for direct API users, not OpenCredits)
if (!subscriptionType && !hasOpenCreditsKey && (message.data.currentCost || message.data.currentDuration)) {
const currentCostStr = message.data.currentCost ? \`$\${message.data.currentCost.toFixed(4)}\` : 'N/A';
const currentDurationStr = message.data.currentDuration ? \`\${message.data.currentDuration}ms\` : 'N/A';
addMessage(\`Request completed - Cost: \${currentCostStr}, Duration: \${currentDurationStr}\`, 'system');
}
break;
case 'accountInfo':
// Store subscription type to determine cost vs plan display
subscriptionType = message.data.subscriptionType || null;
// Update status bar to reflect plan type
updateStatusWithTotals();
break;
case 'modelSwitching':
// Model is being switched (router restarting)
currentModel = message.model;
updateModelSelectionUI();
break;
case 'modelSwitched':
// Model switch complete
currentModel = message.model;
updateModelSelectionUI();
updateStatusWithTotals();
break;
case 'sessionResumed':
showSessionInfo(message.data.sessionId);
addMessage(\`📝 Resumed previous session\\n🆔 Session ID: \${message.data.sessionId}\\n💡 Your conversation history is preserved\`, 'system');
break;
case 'sessionCleared':
// Clear all messages from UI
messagesDiv.innerHTML = '';
hideSessionInfo();
addMessage('🆕 Started new session', 'system');
// Reset totals
totalCost = 0;
totalTokensInput = 0;
totalTokensOutput = 0;
requestCount = 0;
updateStatusWithTotals();
break;
case 'compacting':
if (message.data.isCompacting) {
addMessage('📦 Compacting conversation...', 'system');
}
break;
case 'compactBoundary':
// Reset token counts since conversation was compacted
totalTokensInput = 0;
totalTokensOutput = 0;
updateStatusWithTotals();
const preTokens = message.data.preTokens ? message.data.preTokens.toLocaleString() : 'unknown';
addMessage('✅ Compacted (' + preTokens + ' tokens → summary)', 'system');
break;
case 'loginRequired':
sendStats('Login required');
addMessage('Login required. Please login in the terminal and then come back.', 'error');
updateStatus('Login Required', 'error');
break;
case 'showLoginOptions':
showLoginOptionsModal();
break;
case 'showInstallModal':
sendStats('Claude not installed');
showInstallModal();
updateStatus('Claude Code not installed', 'error');
break;
case 'installComplete':
handleInstallComplete(message.success, message.error);
if (message.success) {
updateStatus('Ready', 'success');
}
break;
case 'showRestoreOption':
showRestoreContainer(message.data);
break;
case 'restoreProgress':
addMessage('🔄 ' + message.data, 'system');
break;
case 'restoreSuccess':
//hideRestoreContainer(message.data.commitSha);
addMessage('✅ ' + message.data.message, 'system');
break;
case 'restoreError':
addMessage('❌ ' + message.data, 'error');
break;
case 'workspaceFiles':
filteredFiles = message.data;
selectedFileIndex = -1;
renderFileList();
break;
case 'imageAttached':
if (message.filePath && message.previewUri) {
addAttachedImage(message.filePath, message.previewUri);
}
break;
case 'conversationList':
displayConversationList(message.data);
break;
case 'clipboardText':
handleClipboardText(message.data);
break;
case 'modelSelected':
// Update the UI with the current model
currentModel = message.model;
selectModel(message.model, true);
break;
case 'terminalOpened':
// Display notification about checking the terminal
addMessage(message.data, 'system');
break;
case 'permissionRequest':
addPermissionRequestMessage(message.data);
break;
case 'askUserQuestion':
addAskUserQuestionMessage(message.data);
break;
case 'updateAskUserQuestionStatus':
updateAskUserQuestionStatus(message.data.id, message.data.status, message.data.answers);
break;
case 'updatePermissionStatus':
updatePermissionStatus(message.data.id, message.data.status);
break;
case 'expirePendingPermissions':
expireAllPendingPermissions();
break;
case 'mcpServers':
displayMCPServers(message.data);
break;
case 'mcpServerSaved':
loadMCPServers(); // Reload the servers list
addMessage('✅ MCP server "' + message.data.name + '" saved successfully', 'system');
break;
case 'mcpServerDeleted':
loadMCPServers(); // Reload the servers list
addMessage('✅ MCP server "' + message.data.name + '" deleted successfully', 'system');
break;
case 'mcpServerError':
addMessage('❌ Error with MCP server: ' + message.data.error, 'error');
break;
case 'marketplaceResponse':
handleMarketplaceResponse(message.data, lastSearchQuery);
break;
case 'marketplaceError':
if (pendingRegistryResponses > 0) {
pendingRegistryResponses--;
if (pendingRegistryResponses === 0) {
// All registries failed or responded — render what we have
handleMarketplaceResponse({ servers: [] }, lastSearchQuery);
}
} else {
var grid = document.getElementById('marketplaceGrid');
if (grid) grid.innerHTML = '<div class="marketplace-loading">Failed to load servers. Check your connection.</div>';
}
break;
case 'skillsList':
displaySkills(message.data);
break;
case 'skillSaved':
loadInstalledSkills();
break;
case 'skillDeleted':
loadInstalledSkills();
break;
case 'skillsSearchResponse':
handleSkillsSearchResponse(message.data);
break;
case 'skillInstallFailed':
break;
case 'pluginsList':
displayPlugins(message.data);
break;
case 'pluginInstalled':
case 'pluginRemoved':
loadInstalledPlugins();
break;
}
});
// Permission request functions
function addPermissionRequestMessage(data) {
const messagesDiv = document.getElementById('messages');
const shouldScroll = shouldAutoScroll(messagesDiv);
const messageDiv = document.createElement('div');
messageDiv.className = 'message permission-request';
messageDiv.id = \`permission-\${data.id}\`;
messageDiv.dataset.status = data.status || 'pending';
let toolName = data.tool || 'Unknown Tool';
if (toolName === 'ExitPlanMode') toolName = 'Approve Plan';
const status = data.status || 'pending';
// Create always allow button text with command styling for Bash
let alwaysAllowText = \`Always allow \${toolName}\`;
let alwaysAllowTooltip = '';
if (toolName === 'Bash' && data.pattern) {
const pattern = data.pattern;
// Remove the asterisk for display - show "npm i" instead of "npm i *"
const displayPattern = pattern.replace(' *', '');
const truncatedPattern = displayPattern.length > 30 ? displayPattern.substring(0, 30) + '...' : displayPattern;
alwaysAllowText = \`Always allow <code>\${truncatedPattern}</code>\`;
alwaysAllowTooltip = displayPattern.length > 30 ? \`title="\${displayPattern}"\` : '';
}
// Show different content based on status
let contentHtml = '';
if (status === 'pending') {
contentHtml = \`
<div class="permission-header">
<span class="icon">🔐</span>
<span>Permission Required</span>
<div class="permission-menu">
<button class="permission-menu-btn" onclick="togglePermissionMenu('\${data.id}')" title="More options">⋮</button>
<div class="permission-menu-dropdown" id="permissionMenu-\${data.id}" style="display: none;">
<button class="permission-menu-item" onclick="enableYoloMode('\${data.id}')">
<span class="menu-icon">⚡</span>
<div class="menu-content">
<span class="menu-title">Enable YOLO Mode</span>
<span class="menu-subtitle">Auto-allow all permissions</span>
</div>
</button>
</div>
</div>
</div>
<div class="permission-content">
<p>\${data.tool === 'ExitPlanMode' ? 'Approve the plan above?' : 'Allow <strong>' + toolName + '</strong> to execute the tool call above?'}</p>
<div class="permission-buttons">
<button class="btn deny" onclick="respondToPermission('\${data.id}', false)">Deny</button>
\${data.tool === 'ExitPlanMode' ? '' : '<button class="btn always-allow" onclick="respondToPermission(\\'' + data.id + '\\', true, true)" ' + alwaysAllowTooltip + '>' + alwaysAllowText + '</button>'}
<button class="btn allow" onclick="respondToPermission('\${data.id}', true)">\${data.tool === 'ExitPlanMode' ? 'Approve' : 'Allow'}</button>
</div>
</div>
\`;
} else if (status === 'approved') {
contentHtml = \`
<div class="permission-header">
<span class="icon">🔐</span>
<span>Permission Required</span>
</div>
<div class="permission-content">
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
<div class="permission-decision allowed">✅ You allowed this</div>
</div>
\`;
messageDiv.classList.add('permission-decided', 'allowed');
} else if (status === 'denied') {
contentHtml = \`
<div class="permission-header">
<span class="icon">🔐</span>
<span>Permission Required</span>
</div>
<div class="permission-content">
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
<div class="permission-decision denied">❌ You denied this</div>
</div>
\`;
messageDiv.classList.add('permission-decided', 'denied');
} else if (status === 'cancelled' || status === 'expired') {
contentHtml = \`
<div class="permission-header">
<span class="icon">🔐</span>
<span>Permission Required</span>
</div>
<div class="permission-content">
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
<div class="permission-decision expired">⏱️ This request expired</div>
</div>
\`;
messageDiv.classList.add('permission-decided', 'expired');
}
messageDiv.innerHTML = contentHtml;
messagesDiv.appendChild(messageDiv);
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
}
function updatePermissionStatus(id, status) {
const permissionMsg = document.getElementById(\`permission-\${id}\`);
if (!permissionMsg) return;
permissionMsg.dataset.status = status;
const permissionContent = permissionMsg.querySelector('.permission-content');
const buttons = permissionMsg.querySelector('.permission-buttons');
const menuDiv = permissionMsg.querySelector('.permission-menu');
// Hide buttons and menu if present
if (buttons) buttons.style.display = 'none';
if (menuDiv) menuDiv.style.display = 'none';
// Remove existing decision div if any
const existingDecision = permissionContent.querySelector('.permission-decision');
if (existingDecision) existingDecision.remove();
// Add new decision div
const decisionDiv = document.createElement('div');
if (status === 'approved') {
decisionDiv.className = 'permission-decision allowed';
decisionDiv.innerHTML = '✅ You allowed this';
permissionMsg.classList.add('permission-decided', 'allowed');
} else if (status === 'denied') {
decisionDiv.className = 'permission-decision denied';
decisionDiv.innerHTML = '❌ You denied this';
permissionMsg.classList.add('permission-decided', 'denied');
} else if (status === 'cancelled' || status === 'expired') {
decisionDiv.className = 'permission-decision expired';
decisionDiv.innerHTML = '⏱️ This request expired';
permissionMsg.classList.add('permission-decided', 'expired');
}
permissionContent.appendChild(decisionDiv);
}
function expireAllPendingPermissions() {
document.querySelectorAll('.permission-request').forEach(permissionMsg => {
if (permissionMsg.dataset.status === 'pending') {
const id = permissionMsg.id.replace('permission-', '');
updatePermissionStatus(id, 'expired');
}
});
}
function respondToPermission(id, approved, alwaysAllow = false) {
// Send response back to extension
vscode.postMessage({
type: 'permissionResponse',
id: id,
approved: approved,
alwaysAllow: alwaysAllow
});
// Update the UI to show the decision
const permissionMsg = document.querySelector(\`.permission-request:has([onclick*="\${id}"])\`);
if (permissionMsg) {
const buttons = permissionMsg.querySelector('.permission-buttons');
const permissionContent = permissionMsg.querySelector('.permission-content');
let decision = approved ? 'You allowed this' : 'You denied this';
if (alwaysAllow && approved) {
decision = 'You allowed this and set it to always allow';
}
const emoji = approved ? '✅' : '❌';
const decisionClass = approved ? 'allowed' : 'denied';
// Hide buttons
buttons.style.display = 'none';
// Add decision div to permission-content
const decisionDiv = document.createElement('div');
decisionDiv.className = \`permission-decision \${decisionClass}\`;
decisionDiv.innerHTML = \`\${emoji} \${decision}\`;
permissionContent.appendChild(decisionDiv);
permissionMsg.classList.add('permission-decided', decisionClass);
}
}
function togglePermissionMenu(permissionId) {
const menu = document.getElementById(\`permissionMenu-\${permissionId}\`);
const isVisible = menu.style.display !== 'none';
// Close all other permission menus
document.querySelectorAll('.permission-menu-dropdown').forEach(dropdown => {
dropdown.style.display = 'none';
});
// Toggle this menu
menu.style.display = isVisible ? 'none' : 'block';
}
function enableYoloMode(permissionId) {
sendStats('YOLO mode enabled');
// Hide the menu
document.getElementById(\`permissionMenu-\${permissionId}\`).style.display = 'none';
// Send message to enable YOLO mode
vscode.postMessage({
type: 'enableYoloMode'
});
// Auto-approve this permission
respondToPermission(permissionId, true);
// Show notification
addMessage('⚡ YOLO Mode enabled! All future permissions will be automatically allowed.', 'system');
}
// Close permission menus when clicking outside
document.addEventListener('click', function(event) {
if (!event.target.closest('.permission-menu')) {
document.querySelectorAll('.permission-menu-dropdown').forEach(dropdown => {
dropdown.style.display = 'none';
});
}
});
// AskUserQuestion functions
function addAskUserQuestionMessage(data) {
const messagesDiv = document.getElementById('messages');
const shouldScroll = shouldAutoScroll(messagesDiv);
var status = data.status || 'pending';
var isResolved = (status === 'answered' || status === 'expired' || status === 'cancelled');
const messageDiv = document.createElement('div');
messageDiv.className = 'message ask-user-question';
messageDiv.id = 'ask-question-' + data.id;
messageDiv.dataset.status = status;
messageDiv.dataset.requestId = data.id;
const questions = data.questions || [];
let questionsHtml = '';
questions.forEach(function(q, idx) {
var header = q.header ? '<div class="question-header">' + escapeHtml(q.header) + '</div>' : '';
var questionText = '<div class="question-text">' + escapeHtml(q.question) + '</div>';
var optionsHtml = '';
if (q.options && q.options.length > 0) {
var inputType = q.multiSelect ? 'checkbox' : 'radio';
var inputName = 'question-' + data.id + '-' + idx;
optionsHtml = '<div class="question-options">';
q.options.forEach(function(opt, optIdx) {
var optId = 'opt-' + data.id + '-' + idx + '-' + optIdx;
var disabled = isResolved ? ' disabled' : '';
optionsHtml += '<label class="question-option" for="' + optId + '">' +
'<input type="' + inputType + '" id="' + optId + '" name="' + inputName + '" value="' + escapeHtml(opt.label) + '"' + disabled + ' />' +
'<div class="option-content">' +
'<span class="option-label">' + escapeHtml(opt.label) + '</span>' +
(opt.description ? '<span class="option-description">' + escapeHtml(opt.description) + '</span>' : '') +
'</div></label>';
});
optionsHtml += '</div>';
}
var freeTextHtml = '<div class="question-freetext">' +
'<input type="text" class="question-freetext-input" data-question-idx="' + idx + '" placeholder="Type your answer..."' + (isResolved ? ' disabled' : '') + ' />' +
'</div>';
questionsHtml += '<div class="question-block" data-question-idx="' + idx + '" data-question="' + escapeHtml(q.question) + '">' +
header + questionText + optionsHtml + freeTextHtml + '</div>';
});
var buttonsHtml = isResolved ? '' :
'<div class="ask-question-buttons">' +
'<button class="btn allow" onclick="submitAskUserQuestionAnswers(\\'' + data.id + '\\')">Submit</button>' +
'</div>';
var decisionHtml = '';
if (status === 'answered' && data.answers) {
decisionHtml = '<div class="ask-question-decision allowed">';
for (var question in data.answers) {
if (data.answers.hasOwnProperty(question)) {
decisionHtml += '<div><strong>' + escapeHtml(question) + '</strong>: ' + escapeHtml(data.answers[question]) + '</div>';
}
}
decisionHtml += '</div>';
} else if (status === 'expired' || status === 'cancelled') {
decisionHtml = '<div class="ask-question-decision expired">This question expired</div>';
}
messageDiv.innerHTML = '<div class="ask-question-header">' +
'<span class="icon">&#10067;</span>' +
'<span>Claude has a question</span>' +
'</div>' +
'<div class="ask-question-content">' +
questionsHtml +
buttonsHtml +
decisionHtml +
'</div>';
if (isResolved) {
messageDiv.classList.add('ask-question-decided');
}
messagesDiv.appendChild(messageDiv);
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
// Focus the first input only if pending
if (!isResolved) {
var firstInput = messageDiv.querySelector('.question-option input, .question-freetext-input');
if (firstInput) firstInput.focus();
}
}
function submitAskUserQuestionAnswers(requestId) {
var container = document.getElementById('ask-question-' + requestId);
if (!container) return;
var questionBlocks = container.querySelectorAll('.question-block');
var answers = {};
questionBlocks.forEach(function(block) {
var questionText = block.dataset.question;
var freeTextInput = block.querySelector('.question-freetext-input');
var freeText = freeTextInput ? freeTextInput.value.trim() : '';
if (freeText) {
answers[questionText] = freeText;
} else {
var checkedInputs = block.querySelectorAll('.question-options input:checked');
if (checkedInputs.length > 0) {
var labels = Array.from(checkedInputs).map(function(input) { return input.value; });
answers[questionText] = labels.join(', ');
}
}
});
vscode.postMessage({
type: 'askUserQuestionResponse',
id: requestId,
answers: answers
});
}
function updateAskUserQuestionStatus(id, status, answers) {
var container = document.getElementById('ask-question-' + id);
if (!container) return;
container.dataset.status = status;
var content = container.querySelector('.ask-question-content');
var buttons = container.querySelector('.ask-question-buttons');
if (buttons) buttons.style.display = 'none';
// Disable all inputs
container.querySelectorAll('input').forEach(function(input) {
input.disabled = true;
});
if (status === 'answered' && answers) {
var summaryDiv = document.createElement('div');
summaryDiv.className = 'ask-question-decision allowed';
var summaryHtml = '';
for (var question in answers) {
if (answers.hasOwnProperty(question)) {
summaryHtml += '<div><strong>' + escapeHtml(question) + '</strong>: ' + escapeHtml(answers[question]) + '</div>';
}
}
summaryDiv.innerHTML = summaryHtml;
content.appendChild(summaryDiv);
} else if (status === 'cancelled') {
var cancelDiv = document.createElement('div');
cancelDiv.className = 'ask-question-decision expired';
cancelDiv.innerHTML = 'This question expired';
content.appendChild(cancelDiv);
}
container.classList.add('ask-question-decided');
}
window.submitAskUserQuestionAnswers = submitAskUserQuestionAnswers;
// Session management functions
function newSession() {
sendStats('New chat');
attachedImages = [];
renderImagePreviews();
vscode.postMessage({
type: 'newSession'
});
}
function restoreToCommit(commitSha) {
vscode.postMessage({
type: 'restoreCommit',
commitSha: commitSha
});
}
function showRestoreContainer(data) {
const messagesDiv = document.getElementById('messages');
const shouldScroll = shouldAutoScroll(messagesDiv);
const restoreContainer = document.createElement('div');
restoreContainer.className = 'restore-container';
restoreContainer.id = \`restore-\${data.sha}\`;
const timeAgo = new Date(data.timestamp).toLocaleTimeString();
const shortSha = data.sha ? data.sha.substring(0, 8) : 'unknown';
restoreContainer.innerHTML = \`
<button class="restore-btn dark" onclick="restoreToCommit('\${data.sha}')">
Restore checkpoint
</button>
<span class="restore-date">\${timeAgo}</span>
\`;
messagesDiv.appendChild(restoreContainer);
scrollToBottomIfNeeded(messagesDiv, shouldScroll);
}
function hideRestoreContainer(commitSha) {
const container = document.getElementById(\`restore-\${commitSha}\`);
if (container) {
container.remove();
}
}
function showSessionInfo(sessionId) {
// const sessionInfo = document.getElementById('sessionInfo');
// const sessionIdSpan = document.getElementById('sessionId');
const sessionStatus = document.getElementById('sessionStatus');
const newSessionBtn = document.getElementById('newSessionBtn');
const historyBtn = document.getElementById('historyBtn');
if (sessionStatus && newSessionBtn) {
// sessionIdSpan.textContent = sessionId.substring(0, 8);
// sessionIdSpan.title = \`Full session ID: \${sessionId} (click to copy)\`;
// sessionIdSpan.style.cursor = 'pointer';
// sessionIdSpan.onclick = () => copySessionId(sessionId);
// sessionInfo.style.display = 'flex';
sessionStatus.style.display = 'none';
newSessionBtn.style.display = 'block';
if (historyBtn) historyBtn.style.display = 'block';
}
}
function copySessionId(sessionId) {
navigator.clipboard.writeText(sessionId).then(() => {
// Show temporary feedback
const sessionIdSpan = document.getElementById('sessionId');
if (sessionIdSpan) {
const originalText = sessionIdSpan.textContent;
sessionIdSpan.textContent = 'Copied!';
setTimeout(() => {
sessionIdSpan.textContent = originalText;
}, 1000);
}
}).catch(err => {
console.error('Failed to copy session ID:', err);
});
}
function hideSessionInfo() {
// const sessionInfo = document.getElementById('sessionInfo');
const sessionStatus = document.getElementById('sessionStatus');
const newSessionBtn = document.getElementById('newSessionBtn');
const historyBtn = document.getElementById('historyBtn');
if (sessionStatus && newSessionBtn) {
// sessionInfo.style.display = 'none';
sessionStatus.style.display = 'none';
// Always show new session
newSessionBtn.style.display = 'block';
// Keep history button visible - don't hide it
if (historyBtn) historyBtn.style.display = 'block';
}
}
updateStatus('Initializing...', 'disconnected');
function parseSimpleMarkdown(markdown) {
// First, handle code blocks before line-by-line processing
let processedMarkdown = markdown;
// Store code blocks temporarily to protect them from further processing
const codeBlockPlaceholders = [];
// Handle multi-line code blocks with triple backticks
// Using RegExp constructor to avoid backtick conflicts in template literal
const codeBlockRegex = new RegExp('\\\`\\\`\\\`(\\\\w*)\\n([\\\\s\\\\S]*?)\\\`\\\`\\\`', 'g');
processedMarkdown = processedMarkdown.replace(codeBlockRegex, function(match, lang, code) {
const language = lang || 'plaintext';
// Process code line by line to preserve formatting like diff implementation
const codeLines = code.split('\\n');
let codeHtml = '';
for (const line of codeLines) {
const escapedLine = escapeHtml(line);
codeHtml += '<div class="code-line">' + escapedLine + '</div>';
}
// Create unique ID for this code block
const codeId = 'code_' + Math.random().toString(36).substr(2, 9);
const escapedCode = escapeHtml(code);
const codeBlockHtml = '<div class="code-block-container"><div class="code-block-header"><span class="code-block-language">' + language + '</span><button class="code-copy-btn" onclick="copyCodeBlock(\\\'' + codeId + '\\\')" title="Copy code"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg></button></div><pre class="code-block"><code class="language-' + language + '" id="' + codeId + '" data-raw-code="' + escapedCode.replace(/"/g, '&quot;') + '">' + codeHtml + '</code></pre></div>';
// Store the code block and return a placeholder
const placeholder = '__CODEBLOCK_' + codeBlockPlaceholders.length + '__';
codeBlockPlaceholders.push(codeBlockHtml);
return placeholder;
});
// Handle inline code with single backticks
const inlineCodeRegex = new RegExp('\\\`([^\\\`]+)\\\`', 'g');
processedMarkdown = processedMarkdown.replace(inlineCodeRegex, '<code>$1</code>');
const lines = processedMarkdown.split('\\n');
let html = '';
let inUnorderedList = false;
let inOrderedList = false;
for (let line of lines) {
line = line.trim();
// Check if this is a code block placeholder
if (line.startsWith('__CODEBLOCK_') && line.endsWith('__')) {
// This is a code block placeholder, don't process it
html += line;
continue;
}
// Bold
line = line.replace(/\\*\\*(.*?)\\*\\*/g, '<strong>$1</strong>');
// Italic - only apply when underscores are surrounded by whitespace or at beginning/end
line = line.replace(/(?<!\\*)\\*(?!\\*)(.*?)\\*(?!\\*)/g, '<em>$1</em>');
line = line.replace(/(^|\\s)_([^_\\s][^_]*[^_\\s]|[^_\\s])_(?=\\s|$)/g, '$1<em>$2</em>');
// Headers
if (/^####\\s+/.test(line)) {
html += '<h4>' + line.replace(/^####\\s+/, '') + '</h4>';
continue;
} else if (/^###\\s+/.test(line)) {
html += '<h3>' + line.replace(/^###\\s+/, '') + '</h3>';
continue;
} else if (/^##\\s+/.test(line)) {
html += '<h2>' + line.replace(/^##\\s+/, '') + '</h2>';
continue;
} else if (/^#\\s+/.test(line)) {
html += '<h1>' + line.replace(/^#\\s+/, '') + '</h1>';
continue;
}
// Ordered list
if (/^\\d+\\.\\s+/.test(line)) {
if (!inOrderedList) {
html += '<ol>';
inOrderedList = true;
}
const item = line.replace(/^\\d+\\.\\s+/, '');
html += '<li>' + item + '</li>';
continue;
}
// Unordered list
if (line.startsWith('- ')) {
if (!inUnorderedList) {
html += '<ul>';
inUnorderedList = true;
}
html += '<li>' + line.slice(2) + '</li>';
continue;
}
// Close lists
if (inUnorderedList) {
html += '</ul>';
inUnorderedList = false;
}
if (inOrderedList) {
html += '</ol>';
inOrderedList = false;
}
// Paragraph or break
if (line !== '') {
html += '<p>' + line + '</p>';
} else {
html += '<br>';
}
}
if (inUnorderedList) html += '</ul>';
if (inOrderedList) html += '</ol>';
// Restore code block placeholders
for (let i = 0; i < codeBlockPlaceholders.length; i++) {
const placeholder = '__CODEBLOCK_' + i + '__';
html = html.replace(placeholder, codeBlockPlaceholders[i]);
}
return html;
}
// Conversation history functions
function toggleConversationHistory() {
const historyDiv = document.getElementById('conversationHistory');
const chatContainer = document.getElementById('chatContainer');
if (historyDiv.style.display === 'none') {
sendStats('History opened');
// Show conversation history
requestConversationList();
historyDiv.style.display = 'block';
chatContainer.style.display = 'none';
} else {
// Hide conversation history
historyDiv.style.display = 'none';
chatContainer.style.display = 'flex';
}
}
function requestConversationList() {
vscode.postMessage({
type: 'getConversationList'
});
}
function loadConversation(filename) {
vscode.postMessage({
type: 'loadConversation',
filename: filename
});
// Hide conversation history and show chat
toggleConversationHistory();
}
// File picker functions
function showFilePicker() {
// Request initial file list from VS Code
vscode.postMessage({
type: 'getWorkspaceFiles',
searchTerm: ''
});
// Show modal
filePickerModal.style.display = 'flex';
fileSearchInput.focus();
selectedFileIndex = -1;
}
function hideFilePicker() {
filePickerModal.style.display = 'none';
fileSearchInput.value = '';
selectedFileIndex = -1;
}
function getFileIcon(filename) {
const ext = filename.split('.').pop()?.toLowerCase();
switch (ext) {
case 'js': case 'jsx': case 'ts': case 'tsx': return '📄';
case 'html': case 'htm': return '🌐';
case 'css': case 'scss': case 'sass': return '🎨';
case 'json': return '📋';
case 'md': return '📝';
case 'py': return '🐍';
case 'java': return '☕';
case 'cpp': case 'c': case 'h': return '⚙️';
case 'png': case 'jpg': case 'jpeg': case 'gif': case 'svg': return '🖼️';
case 'pdf': return '📄';
case 'zip': case 'tar': case 'gz': return '📦';
default: return '📄';
}
}
function renderFileList() {
fileList.innerHTML = '';
filteredFiles.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
if (index === selectedFileIndex) {
fileItem.classList.add('selected');
}
fileItem.innerHTML = \`
<span class="file-icon">\${getFileIcon(file.name)}</span>
<div class="file-info">
<div class="file-name">\${file.name}</div>
<div class="file-path">\${file.path}</div>
</div>
\`;
fileItem.addEventListener('click', () => {
selectFile(file);
});
fileList.appendChild(fileItem);
});
}
function selectFile(file) {
// Insert file path at cursor position
const cursorPos = messageInput.selectionStart;
const textBefore = messageInput.value.substring(0, cursorPos);
const textAfter = messageInput.value.substring(cursorPos);
// Replace the @ symbol with the file path
const beforeAt = textBefore.substring(0, textBefore.lastIndexOf('@'));
const newText = beforeAt + '@' + file.path + ' ' + textAfter;
messageInput.value = newText;
messageInput.focus();
// Set cursor position after the inserted path
const newCursorPos = beforeAt.length + file.path.length + 2;
messageInput.setSelectionRange(newCursorPos, newCursorPos);
hideFilePicker();
adjustTextareaHeight();
}
function filterFiles(searchTerm) {
// Send search request to backend instead of filtering locally
vscode.postMessage({
type: 'getWorkspaceFiles',
searchTerm: searchTerm
});
selectedFileIndex = -1;
}
// Image handling functions
function selectImage() {
// Use VS Code's native file picker instead of browser file picker
vscode.postMessage({
type: 'selectImageFile'
});
}
function addAttachedImage(filePath, previewUri) {
attachedImages.push({ filePath, previewUri });
renderImagePreviews();
messageInput.focus();
}
function removeAttachedImage(index) {
attachedImages.splice(index, 1);
renderImagePreviews();
}
function renderImagePreviews() {
const container = document.getElementById('imagePreviewContainer');
if (attachedImages.length === 0) {
container.style.display = 'none';
container.innerHTML = '';
return;
}
container.style.display = 'flex';
container.innerHTML = attachedImages.map((img, i) =>
'<div class="image-preview-item">' +
'<img src="' + img.previewUri + '" />' +
'<button class="image-preview-remove" onclick="removeAttachedImage(' + i + ')">&times;</button>' +
'</div>'
).join('');
}
function showImageAddedFeedback(fileName) {
// Create temporary feedback element
const feedback = document.createElement('div');
feedback.textContent = \`Added: \${fileName}\`;
feedback.style.cssText = \`
position: fixed;
top: 20px;
right: 20px;
background: var(--vscode-notifications-background);
color: var(--vscode-notifications-foreground);
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
\`;
document.body.appendChild(feedback);
// Animate in
setTimeout(() => feedback.style.opacity = '1', 10);
// Animate out and remove
setTimeout(() => {
feedback.style.opacity = '0';
setTimeout(() => feedback.remove(), 300);
}, 2000);
}
function displayConversationList(conversations) {
const listDiv = document.getElementById('conversationList');
listDiv.innerHTML = '';
if (conversations.length === 0) {
listDiv.innerHTML = '<p style="text-align: center; color: var(--vscode-descriptionForeground);">No conversations found</p>';
return;
}
conversations.forEach(conv => {
const item = document.createElement('div');
item.className = 'conversation-item';
item.onclick = () => loadConversation(conv.filename);
const date = new Date(conv.startTime).toLocaleDateString();
const time = new Date(conv.startTime).toLocaleTimeString();
// Show plan type or cost based on subscription
let usageStr;
if (subscriptionType) {
let planName = subscriptionType.replace(/^claude\\s*/i, '').trim();
planName = planName.charAt(0).toUpperCase() + planName.slice(1);
usageStr = planName;
} else {
usageStr = \`$\${conv.totalCost.toFixed(3)}\`;
}
item.innerHTML = \`
<div class="conversation-title">\${conv.firstUserMessage.substring(0, 60)}\${conv.firstUserMessage.length > 60 ? '...' : ''}</div>
<div class="conversation-meta">\${date} at \${time}\${conv.messageCount} messages • \${usageStr}</div>
<div class="conversation-preview">Last: \${conv.lastUserMessage.substring(0, 80)}\${conv.lastUserMessage.length > 80 ? '...' : ''}</div>
\`;
listDiv.appendChild(item);
});
}
function handleClipboardText(text) {
if (!text) return;
// Insert text at cursor position
const start = messageInput.selectionStart;
const end = messageInput.selectionEnd;
const currentValue = messageInput.value;
const newValue = currentValue.substring(0, start) + text + currentValue.substring(end);
messageInput.value = newValue;
// Set cursor position after pasted text
const newCursorPos = start + text.length;
messageInput.setSelectionRange(newCursorPos, newCursorPos);
// Trigger input event to adjust height
messageInput.dispatchEvent(new Event('input', { bubbles: true }));
}
// Settings functions
function toggleSettings() {
const settingsModal = document.getElementById('settingsModal');
if (settingsModal.style.display === 'none') {
// Request current settings from VS Code
vscode.postMessage({
type: 'getSettings'
});
// Request current permissions
vscode.postMessage({
type: 'getPermissions'
});
settingsModal.style.display = 'flex';
} else {
hideSettingsModal();
}
}
function hideSettingsModal() {
document.getElementById('settingsModal').style.display = 'none';
}
function showSupportModal() {
document.getElementById('supportModal').style.display = 'flex';
}
function hideSupportModal() {
document.getElementById('supportModal').style.display = 'none';
}
async function submitSupport() {
var type = document.getElementById('supportType').value;
var email = document.getElementById('supportEmail').value.trim();
var message = document.getElementById('supportMessage').value.trim();
if (!message) { return; }
sendStats('Support attempted', { type: type });
var btn = document.getElementById('supportSubmitBtn');
btn.textContent = 'Sending...';
btn.disabled = true;
try {
var res = await fetch('https://claudecodechat.com/api/support', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: type, email: email, message: message })
});
if (res.ok) {
btn.textContent = 'Sent!';
sendStats('Support submitted', { type: type });
setTimeout(function() {
hideSupportModal();
document.getElementById('supportMessage').value = '';
document.getElementById('supportEmail').value = '';
btn.textContent = 'Send';
btn.disabled = false;
}, 1500);
} else {
btn.textContent = 'Failed — try again';
btn.disabled = false;
}
} catch (e) {
btn.textContent = 'Failed — try again';
btn.disabled = false;
}
}
function updateSettings() {
// Note: thinking intensity is now handled separately in the thinking intensity modal
const wslEnabled = document.getElementById('wsl-enabled').checked;
const wslDistro = document.getElementById('wsl-distro').value;
const wslNodePath = document.getElementById('wsl-node-path').value;
const wslClaudePath = document.getElementById('wsl-claude-path').value;
const yoloMode = document.getElementById('yolo-mode').checked;
const executablePath = document.getElementById('executable-path').value;
const useRouter = document.getElementById('use-router')?.checked || false;
// Collect environment variables from key-value UI
const envVariables = getEnvVariablesFromUI();
// Update WSL options visibility
document.getElementById('wslOptions').style.display = wslEnabled ? 'block' : 'none';
// Update OpenCredits state from current env vars
const baseUrl = envVariables['ANTHROPIC_BASE_URL'] || '';
const wasOpenCredits = hasOpenCreditsKey;
hasOpenCreditsKey = !!(baseUrl && (baseUrl.includes('opencredits') || baseUrl.includes('localhost:8787')));
if (!hasOpenCreditsKey) {
openCreditsBalance = null;
// If a OpenCredits model was selected, revert to default
if (wasOpenCredits && isOpenCreditsModel(currentModel)) {
selectModel('default');
}
}
updateStatusWithTotals();
updateOpenCreditsPromo();
// Send settings to VS Code immediately
sendStats('Settings changed', {
wsl_enabled: wslEnabled,
yolo_mode: yoloMode,
router_enabled: useRouter,
has_custom_envs: Object.keys(envVariables).length > 0,
has_custom_executable: !!executablePath
});
vscode.postMessage({
type: 'updateSettings',
settings: {
'wsl.enabled': wslEnabled,
'wsl.distro': wslDistro || 'Ubuntu',
'wsl.nodePath': wslNodePath || '/usr/bin/node',
'wsl.claudePath': wslClaudePath || '/usr/local/bin/claude',
'permissions.yoloMode': yoloMode,
'executable.path': executablePath,
'environment.variables': envVariables,
'router.enabled': useRouter
}
});
}
// Provider region filtering
function applyProviderExclusion() {
var enabled = document.getElementById('us-eu-providers-only').checked;
var envVars = getEnvVariablesFromUI();
if (enabled) {
envVars['ANTHROPIC_CUSTOM_HEADERS'] = 'X-Only-Providers: US,EU';
} else {
delete envVars['ANTHROPIC_CUSTOM_HEADERS'];
}
vscode.postMessage({
type: 'updateSettings',
settings: {
'environment.variables': envVars
}
});
}
// Environment variables key-value UI functions
function getEnvVariablesFromUI() {
const envVars = {};
const rows = document.querySelectorAll('.env-variable-row');
rows.forEach(row => {
const keyInput = row.querySelector('.env-key');
const valueInput = row.querySelector('.env-value');
if (keyInput && valueInput && keyInput.value.trim()) {
envVars[keyInput.value.trim()] = valueInput.value;
}
});
return envVars;
}
function renderEnvVariables(envVars) {
const container = document.getElementById('env-variables-list');
container.innerHTML = '';
const entries = Object.entries(envVars || {});
if (entries.length === 0) {
// Always show at least one empty row
addEnvVariableRow('', '');
} else {
entries.forEach(([key, value]) => {
addEnvVariableRow(key, value);
});
}
}
function addEnvVariable() {
addEnvVariableRow('', '');
}
function addEnvVariableRow(key, value) {
const container = document.getElementById('env-variables-list');
const row = document.createElement('div');
row.className = 'env-variable-row';
row.innerHTML = '<input type="text" class="env-key" placeholder="KEY" value="' + (key || '') + '" onchange="updateSettings()">' +
'<input type="text" class="env-value" placeholder="value" value="' + (value || '') + '" onchange="updateSettings()">' +
'<button class="env-variable-remove" onclick="removeEnvVariable(this)" title="Remove">✕</button>';
container.appendChild(row);
}
function removeEnvVariable(btn) {
btn.closest('.env-variable-row').remove();
updateSettings();
}
// Permissions management functions
function renderPermissions(permissions) {
const permissionsList = document.getElementById('permissionsList');
if (!permissions || !permissions.alwaysAllow || Object.keys(permissions.alwaysAllow).length === 0) {
permissionsList.innerHTML = \`
<div class="permissions-empty">
No always-allow permissions set
</div>
\`;
return;
}
let html = '';
for (const [toolName, permission] of Object.entries(permissions.alwaysAllow)) {
if (permission === true) {
// Tool is always allowed
html += \`
<div class="permission-item">
<div class="permission-info">
<span class="permission-tool">\${toolName}</span>
<span class="permission-desc">All</span>
</div>
<button class="permission-remove-btn" onclick="removePermission('\${toolName}', null)">Remove</button>
</div>
\`;
} else if (Array.isArray(permission)) {
// Tool has specific commands/patterns
for (const command of permission) {
const displayCommand = command.replace(' *', ''); // Remove asterisk for display
html += \`
<div class="permission-item">
<div class="permission-info">
<span class="permission-tool">\${toolName}</span>
<span class="permission-command"><code>\${displayCommand}</code></span>
</div>
<button class="permission-remove-btn" onclick="removePermission('\${toolName}', '\${escapeHtml(command)}')">Remove</button>
</div>
\`;
}
}
}
permissionsList.innerHTML = html;
}
function removePermission(toolName, command) {
vscode.postMessage({
type: 'removePermission',
toolName: toolName,
command: command
});
}
function showAddPermissionForm() {
document.getElementById('showAddPermissionBtn').style.display = 'none';
document.getElementById('addPermissionForm').style.display = 'block';
// Focus on the tool select dropdown
setTimeout(() => {
document.getElementById('addPermissionTool').focus();
}, 100);
}
function hideAddPermissionForm() {
document.getElementById('showAddPermissionBtn').style.display = 'flex';
document.getElementById('addPermissionForm').style.display = 'none';
// Clear form inputs
document.getElementById('addPermissionTool').value = '';
document.getElementById('addPermissionCommand').value = '';
document.getElementById('addPermissionCommand').style.display = 'none';
}
function toggleCommandInput() {
const toolSelect = document.getElementById('addPermissionTool');
const commandInput = document.getElementById('addPermissionCommand');
const hintDiv = document.getElementById('permissionsFormHint');
if (toolSelect.value === 'Bash') {
commandInput.style.display = 'block';
hintDiv.textContent = 'Use patterns like "npm i *" or "git add *" for specific commands.';
} else if (toolSelect.value === '') {
commandInput.style.display = 'none';
commandInput.value = '';
hintDiv.textContent = 'Select a tool to add always-allow permission.';
} else {
commandInput.style.display = 'none';
commandInput.value = '';
hintDiv.textContent = 'This will allow all ' + toolSelect.value + ' commands without asking for permission.';
}
}
function addPermission() {
const toolSelect = document.getElementById('addPermissionTool');
const commandInput = document.getElementById('addPermissionCommand');
const addBtn = document.getElementById('addPermissionBtn');
const toolName = toolSelect.value.trim();
const command = commandInput.value.trim();
if (!toolName) {
return;
}
// Disable button during processing
addBtn.disabled = true;
addBtn.textContent = 'Adding...';
vscode.postMessage({
type: 'addPermission',
toolName: toolName,
command: command || null
});
// Clear form and hide it
toolSelect.value = '';
commandInput.value = '';
hideAddPermissionForm();
// Re-enable button
setTimeout(() => {
addBtn.disabled = false;
addBtn.textContent = 'Add';
}, 500);
}
// Close settings modal when clicking outside
document.getElementById('settingsModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('settingsModal')) {
hideSettingsModal();
}
});
// Close thinking intensity modal when clicking outside
document.getElementById('thinkingIntensityModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('thinkingIntensityModal')) {
hideThinkingIntensityModal();
}
});
// Close slash commands modal when clicking outside
document.getElementById('slashCommandsModal').addEventListener('click', (e) => {
if (e.target === document.getElementById('slashCommandsModal')) {
hideSlashCommandsModal();
}
});
// Request custom snippets from VS Code on page load
vscode.postMessage({
type: 'getCustomSnippets'
});
// Detect slash commands input
messageInput.addEventListener('input', (e) => {
const value = messageInput.value;
// Only trigger when "/" is the very first and only character
if (value === '/') {
showSlashCommandsModal();
}
});
// Add settings message handler to window message event
const originalMessageHandler = window.onmessage;
window.addEventListener('message', event => {
const message = event.data;
if (message.type === 'customSnippetsData') {
// Update global custom snippets data
customSnippetsData = message.data || {};
// Refresh the snippets display
loadCustomSnippets(customSnippetsData);
} else if (message.type === 'customSnippetSaved') {
// Refresh snippets after saving
vscode.postMessage({
type: 'getCustomSnippets'
});
} else if (message.type === 'customSnippetDeleted') {
// Refresh snippets after deletion
vscode.postMessage({
type: 'getCustomSnippets'
});
} else if (message.type === 'settingsData') {
// Update UI with current settings
const thinkingIntensity = message.data['thinking.intensity'] || 'think';
const intensityValues = ['think', 'think-hard', 'think-harder', 'ultrathink'];
const sliderValue = intensityValues.indexOf(thinkingIntensity);
// Update thinking intensity modal if it exists
const thinkingIntensitySlider = document.getElementById('thinkingIntensitySlider');
if (thinkingIntensitySlider) {
thinkingIntensitySlider.value = sliderValue >= 0 ? sliderValue : 0;
updateThinkingIntensityDisplay(thinkingIntensitySlider.value);
} else {
// Update toggle name even if modal isn't open
updateThinkingModeToggleName(sliderValue >= 0 ? sliderValue : 0);
}
document.getElementById('wsl-enabled').checked = message.data['wsl.enabled'] || false;
document.getElementById('wsl-distro').value = message.data['wsl.distro'] || 'Ubuntu';
document.getElementById('wsl-node-path').value = message.data['wsl.nodePath'] || '/usr/bin/node';
document.getElementById('wsl-claude-path').value = message.data['wsl.claudePath'] || '/usr/local/bin/claude';
document.getElementById('yolo-mode').checked = message.data['permissions.yoloMode'] || false;
// Update yolo warning visibility
updateYoloWarning();
// Show/hide WSL options
document.getElementById('wslOptions').style.display = message.data['wsl.enabled'] ? 'block' : 'none';
// Update router checkbox
const useRouterCheckbox = document.getElementById('use-router');
if (useRouterCheckbox) {
useRouterCheckbox.checked = message.data['router.enabled'] || false;
}
// Update Customize Claude Command settings
document.getElementById('executable-path').value = message.data['executable.path'] || '';
renderEnvVariables(message.data['environment.variables'] || {});
// Detect OpenCredits and envs disabled state
envsDisabled = !!(message.data['environment.disabled']);
const envVars = message.data['environment.variables'] || {};
const wasOpenCreditsSettings = hasOpenCreditsKey;
hasOpenCreditsKey = !!(message.data['isOpenCredits'] || (!envsDisabled && envVars['ANTHROPIC_BASE_URL'] && (envVars['ANTHROPIC_BASE_URL'].includes('opencredits') || envVars['ANTHROPIC_BASE_URL'].includes('localhost:8787'))));
// Show/hide provider exclusion based on OpenCredits
var providerSection = document.getElementById('providerExclusionSection');
if (providerSection) {
providerSection.style.display = hasOpenCreditsKey ? '' : 'none';
}
var providerHint = document.getElementById('providerExclusionHint');
if (providerHint) {
providerHint.style.display = hasOpenCreditsKey ? '' : 'none';
}
// Restore provider region state from env vars
var customHeaders = envVars['ANTHROPIC_CUSTOM_HEADERS'] || '';
var usEuCheckbox = document.getElementById('us-eu-providers-only');
if (usEuCheckbox) {
usEuCheckbox.checked = customHeaders.indexOf('X-Only-Providers:') !== -1;
}
if (!hasOpenCreditsKey) {
openCreditsBalance = null;
// If a OpenCredits model was selected, revert to default
if (wasOpenCreditsSettings && isOpenCreditsModel(currentModel)) {
selectModel('default', true);
}
}
// If the user manually changed the model env vars, reflect in the UI
var envSonnetModel = envVars['ANTHROPIC_DEFAULT_SONNET_MODEL'];
if (envSonnetModel && envSonnetModel !== currentModel) {
// Check if it matches a known OpenCredits model
var matchingModel = openCreditsModels.find(function(m) { return m.id === envSonnetModel; });
if (matchingModel) {
currentModel = envSonnetModel;
updateModelSelectionUI();
} else if (isOpenCreditsModel(envSonnetModel)) {
// Custom model from env vars - update display
currentModel = envSonnetModel;
updateModelSelectionUI();
}
} else if (!envSonnetModel && !hasOpenCreditsKey && isOpenCreditsModel(currentModel)) {
// OpenCredits was removed and model env var is gone — revert to default
selectModel('default', true);
}
updateOpenCreditsPromo();
updateStatusWithTotals();
}
if (message.type === 'openedExternalUrl') {
const modal = document.getElementById('externalUrlModal');
const btn = document.getElementById('externalUrlFallbackBtn');
const urlDisplay = document.getElementById('externalUrlDisplay');
const copyBtn = document.getElementById('externalUrlCopyBtn');
urlDisplay.textContent = message.url;
modal.style.display = 'flex';
btn.onclick = () => {
vscode.postMessage({ type: 'openExternalUrl', url: message.url });
modal.style.display = 'none';
};
copyBtn.onclick = () => {
navigator.clipboard.writeText(message.url);
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>';
setTimeout(() => {
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>';
}, 1500);
};
}
if (message.type === 'envVarsData' && message.data) {
var d = message.data;
if (comboSonnet && comboSonnet.setValue) comboSonnet.setValue(d['ANTHROPIC_DEFAULT_SONNET_MODEL'] || '');
if (comboOpus && comboOpus.setValue) comboOpus.setValue(d['ANTHROPIC_DEFAULT_OPUS_MODEL'] || '');
if (comboHaiku && comboHaiku.setValue) comboHaiku.setValue(d['ANTHROPIC_DEFAULT_HAIKU_MODEL'] || '');
}
if (message.type === 'opencreditsBalance') {
openCreditsBalance = message.balance;
updateStatusWithTotals();
updateOpenCreditsPromo();
}
if (message.type === 'featureFlags') {
opencreditsEnabled = !!message.opencredits_enabled;
if (opencreditsEnabled) {
sendStats('OpenCredits enabled');
}
renderQuickButtons();
// Check if there's a saved key we can restore
vscode.postMessage({ type: 'checkSavedOpenCredits' });
}
if (message.type === 'savedOpenCreditsStatus') {
hasSavedOpenCreditsKey = !!message.hasSavedKey;
updateOpenCreditsPromo();
}
if (message.type === 'updateRecommendedModels' && message.models) {
openCreditsModels = message.models.map(function(m) {
return {
id: m.id,
name: m.name,
provider: m.provider,
quickLabel: m.quickLabel,
credits_per_request: m.credits_per_request || null,
tierModels: m.tierModels
};
});
// Update credits_per_request from live pricing data
if (message.creditsPricing && message.creditsPricing.models) {
var pricingById = {};
message.creditsPricing.models.forEach(function(p) { pricingById[p.id] = p.credits_per_request; });
openCreditsModels.forEach(function(m) {
if (pricingById[m.id] != null) m.credits_per_request = pricingById[m.id];
});
// Store reference model pricing
creditsPricingData = message.creditsPricing;
}
renderQuickButtons();
renderOpenCreditsModelCards();
}
if (message.type === 'opencreditsKeyReceived') {
hasOpenCreditsKey = true;
}
if (message.type === 'checkoutSaveError') {
showCheckoutError(message.message || 'Failed to save credentials. Please try again.');
}
if (message.type === 'opencreditsActivated') {
hasOpenCreditsKey = true;
openCreditsBalance = message.balance;
if (message.model) {
selectModel(message.model, true);
}
updateStatusWithTotals();
}
if (message.type === 'platformInfo') {
// Check if user is on Windows and show WSL alert if not dismissed and WSL not already enabled
if (message.data.isWindows && !message.data.wslAlertDismissed && !message.data.wslEnabled) {
// Small delay to ensure UI is ready
setTimeout(() => {
showWSLAlert();
}, 1000);
}
}
if (message.type === 'permissionsData') {
// Update permissions UI
renderPermissions(message.data);
}
});
${getSkillsScript()}
${getPluginsScript()}
</script>`
export default getScript;