(function () {
const vscode = acquireVsCodeApi();
const messagesEl = document.getElementById('messages');
const inputEl = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const cancelBtn = document.getElementById('cancel-btn');
let isStreaming = false;
let currentAssistantId = null;
// Auto-resize textarea
inputEl.addEventListener('input', () => {
inputEl.style.height = 'auto';
inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
});
// Send on Enter (Shift+Enter for newline)
inputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
sendBtn.addEventListener('click', sendMessage);
cancelBtn.addEventListener('click', cancelStream);
function sendMessage() {
const text = inputEl.value.trim();
if (!text || isStreaming) return;
vscode.postMessage({ type: 'sendMessage', text });
inputEl.value = '';
inputEl.style.height = 'auto';
inputEl.style.height = '36px';
}
function cancelStream() {
vscode.postMessage({ type: 'cancel' });
setStreaming(false);
}
function setStreaming(streaming) {
isStreaming = streaming;
sendBtn.style.display = streaming ? 'none' : 'block';
cancelBtn.style.display = streaming ? 'block' : 'none';
inputEl.disabled = streaming;
}
function addMessage(msg) {
const msgEl = document.createElement('div');
msgEl.className = `message ${msg.role}`;
msgEl.dataset.id = msg.id;
msgEl.innerHTML = formatContent(msg.content);
const timeEl = document.createElement('div');
timeEl.className = 'message-time';
timeEl.textContent = formatTime(msg.timestamp);
msgEl.appendChild(timeEl);
messagesEl.appendChild(msgEl);
scrollToBottom();
attachCodeActions(msgEl);
}
function appendChunk(id, chunk) {
const msgEl = document.querySelector(`.message[data-id="${id}"]`);
if (!msgEl) return;
const contentEl = msgEl.querySelector('.content') || msgEl;
const existing = contentEl.dataset.raw || '';
const updated = existing + chunk;
contentEl.dataset.raw = updated;
// Re-render the full content
const timeEl = msgEl.querySelector('.message-time');
msgEl.innerHTML = formatContent(updated);
if (timeEl) msgEl.appendChild(timeEl);
scrollToBottom();
attachCodeActions(msgEl);
}
function finalizeMessage(id) {
const msgEl = document.querySelector(`.message[data-id="${id}"]`);
if (msgEl) {
msgEl.classList.remove('streaming');
}
setStreaming(false);
currentAssistantId = null;
}
function showError(id, error) {
const msgEl = document.querySelector(`.message[data-id="${id}"]`) || document.createElement('div');
msgEl.className = 'message error';
msgEl.textContent = error;
if (!msgEl.parentNode) {
messagesEl.appendChild(msgEl);
}
setStreaming(false);
currentAssistantId = null;
scrollToBottom();
}
function formatContent(text) {
if (!text) return '';
// Escape HTML
let html = text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
// Code blocks
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
const escapedCode = code.replace(/^\n|\n$/g, '');
return `<div class="code-block-header">
<span>${lang || 'text'}</span>
<div class="code-block-actions">
<button class="code-btn copy-btn" data-code="${encodeURIComponent(escapedCode)}">Copy</button>
<button class="code-btn insert-btn" data-code="${encodeURIComponent(escapedCode)}">Insert</button>
</div>
</div><pre><code class="language-${lang || 'text'}">${escapedCode}</code></pre>`;
});
// Inline code
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// Bold
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
// Italic
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
html = html.replace(/_([^_]+)_/g, '<em>$1</em>');
// Strikethrough
html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
// Headers
html = html.replace(/^### (.*$)/gim, '<h3>$1</h3>');
html = html.replace(/^## (.*$)/gim, '<h2>$1</h2>');
html = html.replace(/^# (.*$)/gim, '<h1>$1</h1>');
// Lists
html = html.replace(/^\s*[-*+] (.*$)/gim, '<li>$1</li>');
html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
html = html.replace(/<\/ul>\n?<ul>/g, '');
// Ordered lists
html = html.replace(/^\s*\d+\.\s+(.*$)/gim, '<li>$1</li>');
// Blockquotes
html = html.replace(/^> (.*$)/gim, '<blockquote>$1</blockquote>');
html = html.replace(/(<blockquote>.*<\/blockquote>\n?)+/g, (matches) => {
return '<blockquote>' + matches.replace(/<\/?blockquote>/g, '') + '</blockquote>';
});
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
// Horizontal rules
html = html.replace(/^---+$/gim, '<hr/>');
// Paragraphs (wrap remaining text)
const lines = html.split('\n');
let result = '';
let inParagraph = false;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) {
if (inParagraph) {
result += '</p>';
inParagraph = false;
}
result += '\n';
continue;
}
const isBlock = /^<(h[1-6]|ul|ol|pre|blockquote|hr|div)/i.test(trimmed);
if (isBlock) {
if (inParagraph) {
result += '</p>';
inParagraph = false;
}
result += line + '\n';
} else {
if (!inParagraph) {
result += '<p>';
inParagraph = true;
}
result += line + ' ';
}
}
if (inParagraph) {
result += '</p>';
}
return result;
}
function attachCodeActions(container) {
container.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', () => {
const code = decodeURIComponent(btn.dataset.code);
vscode.postMessage({ type: 'copyCode', code });
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = 'Copy', 1500);
});
});
container.querySelectorAll('.insert-btn').forEach(btn => {
btn.addEventListener('click', () => {
const code = decodeURIComponent(btn.dataset.code);
vscode.postMessage({ type: 'insertCode', code });
});
});
}
function scrollToBottom() {
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
function clearMessages() {
messagesEl.innerHTML = '';
}
function showWelcome() {
messagesEl.innerHTML = `
<div class="welcome">
<div class="icon">✨</div>
<h3>Abysius AI</h3>
<p>Ask me anything about your code.<br>I can help with writing, debugging, explaining, and refactoring.</p>
</div>
`;
}
// Initial state
showWelcome();
vscode.postMessage({ type: 'ready' });
// Handle messages from extension
window.addEventListener('message', (event) => {
const msg = event.data;
switch (msg.type) {
case 'history':
clearMessages();
if (msg.messages.length === 0) {
showWelcome();
} else {
msg.messages.forEach(m => addMessage(m));
}
break;
case 'userMessage':
addMessage(msg.message);
break;
case 'assistantStart':
currentAssistantId = msg.id;
setStreaming(true);
const assistantEl = document.createElement('div');
assistantEl.className = 'message assistant streaming';
assistantEl.dataset.id = msg.id;
assistantEl.innerHTML = '<span class="streaming-indicator"><span></span><span></span><span></span></span>';
messagesEl.appendChild(assistantEl);
scrollToBottom();
break;
case 'assistantChunk':
// Remove the loading indicator on first chunk
const el = document.querySelector(`.message[data-id="${msg.id}"]`);
if (el && el.querySelector('.streaming-indicator')) {
el.innerHTML = '';
}
appendChunk(msg.id, msg.chunk);
break;
case 'assistantDone':
finalizeMessage(msg.id);
break;
case 'assistantError':
showError(msg.id, msg.error);
break;
case 'clear':
clearMessages();
showWelcome();
break;
}
});
})();