mirror of
https://github.com/andrepimenta/claude-code-chat.git
synced 2026-05-30 16:45:29 +08:00
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>
5250 lines
191 KiB
TypeScript
5250 lines
191 KiB
TypeScript
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="×",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, '"').replace(/'/g, ''');
|
||
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, ''');
|
||
html += '<button class="plan-action-btn" onclick="sendPlanAction('' + escapedPrompt + '')" 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, '"').replace(/'/g, ''');
|
||
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(/"/g, '"').replace(/'/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(/"/g, '"').replace(/'/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="none"" />' : '<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) + ' ★</span>' : '';
|
||
var typeHtml = installType ? '<span class="marketplace-item-type">' + escapeHtml(installType) + '</span>' : '';
|
||
|
||
var safeId = escapeHtml(server.id || name).replace(/'/g, ''');
|
||
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">★</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="none"" />' : '<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) + ' ★</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, ''');
|
||
|
||
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)">← 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, '"')})">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(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/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">❓</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, '"') + '">' + 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 + ')">×</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; |