import * as vscode from 'vscode';
import { AbysiusApi, ChatMessage, ChatRequest } from './api';
export interface ChatHistoryMessage {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
export class ChatPanel implements vscode.WebviewViewProvider {
private _view?: vscode.WebviewView;
private _api: AbysiusApi;
private _messages: ChatHistoryMessage[] = [];
private _currentStreamAbort?: AbortController;
private _extensionUri: vscode.Uri;
constructor(extensionUri: vscode.Uri, api: AbysiusApi) {
this._extensionUri = extensionUri;
this._api = api;
}
resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [this._extensionUri]
};
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
webviewView.webview.onDidReceiveMessage(
async (message) => {
switch (message.type) {
case 'sendMessage':
await this._handleUserMessage(message.text);
break;
case 'cancel':
this._currentStreamAbort?.abort();
break;
case 'clear':
this.clear();
break;
case 'ready':
this._postMessage({ type: 'history', messages: this._messages });
break;
case 'insertCode':
this._insertCodeIntoEditor(message.code);
break;
case 'copyCode':
vscode.env.clipboard.writeText(message.code);
break;
}
}
);
webviewView.onDidDispose(() => {
this._view = undefined;
});
}
show(): void {
if (this._view) {
this._view.show(true);
} else {
vscode.commands.executeCommand('abysius.chatPanel.focus');
}
}
clear(): void {
this._messages = [];
this._postMessage({ type: 'clear' });
}
private async _handleUserMessage(text: string): Promise<void> {
const userMsg: ChatHistoryMessage = {
id: this._generateId(),
role: 'user',
content: text,
timestamp: Date.now()
};
this._messages.push(userMsg);
this._postMessage({ type: 'userMessage', message: userMsg });
// Gather context from active editor
const context = this._getEditorContext();
const chatMessages: ChatMessage[] = [
{
role: 'system',
content: this._buildSystemPrompt(context)
}
];
// Add recent message history
const recentMessages = this._messages.slice(-10);
for (const msg of recentMessages) {
chatMessages.push({
role: msg.role,
content: msg.content
});
}
const request: ChatRequest = {
messages: chatMessages,
temperature: 0.7,
max_tokens: 4096
};
const assistantId = this._generateId();
this._postMessage({
type: 'assistantStart',
id: assistantId
});
try {
this._currentStreamAbort = new AbortController();
const stream = this._api.streamChat(request, this._currentStreamAbort.signal);
let fullResponse = '';
for await (const chunk of stream) {
fullResponse += chunk;
this._postMessage({
type: 'assistantChunk',
id: assistantId,
chunk: chunk
});
}
const assistantMsg: ChatHistoryMessage = {
id: assistantId,
role: 'assistant',
content: fullResponse,
timestamp: Date.now()
};
this._messages.push(assistantMsg);
this._postMessage({
type: 'assistantDone',
id: assistantId
});
} catch (err) {
this._postMessage({
type: 'assistantError',
id: assistantId,
error: 'Failed to get response from Abysius AI'
});
}
}
private _getEditorContext(): { language?: string; filename?: string; selectedText?: string; cursorLine?: number } {
const editor = vscode.window.activeTextEditor;
if (!editor) return {};
const document = editor.document;
const selection = editor.selection;
return {
language: document.languageId,
filename: document.fileName,
selectedText: selection.isEmpty ? undefined : document.getText(selection),
cursorLine: selection.active.line + 1
};
}
private _buildSystemPrompt(context: { language?: string; filename?: string; selectedText?: string; cursorLine?: number }): string {
let prompt = `You are Abysius, an AI coding assistant embedded in a code editor. You help with programming, debugging, code review, and general software development questions.`;
if (context.language) {
prompt += `\n\nThe user is currently working in a ${context.language} file`;
if (context.filename) {
prompt += ` (${context.filename.split('/').pop()})`;
}
prompt += '.';
}
if (context.selectedText) {
prompt += `\n\nSelected code:\n\`\`\`${context.language || ''}\n${context.selectedText}\n\`\`\``;
}
if (context.cursorLine) {
prompt += `\n\nCursor is at line ${context.cursorLine}.`;
}
prompt += `\n\nWhen providing code, wrap it in markdown code blocks with the appropriate language tag. Be concise and helpful.`;
return prompt;
}
private _insertCodeIntoEditor(code: string): void {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showWarningMessage('No active editor to insert code into');
return;
}
editor.edit(editBuilder => {
const selection = editor.selection;
editBuilder.replace(selection, code);
});
}
private _postMessage(message: any): void {
if (this._view) {
this._view.webview.postMessage(message);
}
}
private _generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
private _getHtmlForWebview(webview: vscode.Webview): string {
const styleUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'assets', 'chat.css')
);
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'assets', 'chat.js')
);
const nonce = this._generateId();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}'; connect-src https:;">
<link href="${styleUri}" rel="stylesheet">
</head>
<body>
<div id="chat-container">
<div id="messages"></div>
<div id="input-area">
<textarea id="message-input" rows="1" placeholder="Ask Abysius..."></textarea>
<button id="send-btn">Send</button>
<button id="cancel-btn" style="display:none;">Stop</button>
</div>
</div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
}
dispose(): void {
this._currentStreamAbort?.abort();
}
}