Added code completion, @file context

main
Jeremy 2025-02-23 15:33:26 -06:00
parent 02ebd7b198
commit 1bd565e16b
25 changed files with 2250 additions and 308 deletions

View File

@ -3,6 +3,7 @@ const { AISidebarProvider } = require('./src/utils/AISidebarProvider');
const LLMSelectorPanel = require('./src/settings/panels/llmSelector');
const SecureSettingsManager = require('./src/settings/secureSettingsManager');
const { RepoTracker } = require('./src/utils/RepoTracker');
const { CodeCompletionProvider } = require('./src/utils/CodeCompletionProvider');
async function activate(context) {
// Initialize secure settings manager first
@ -34,6 +35,12 @@ async function activate(context) {
}
);
// Register Code Completion Provider
const completionProvider = new CodeCompletionProvider(this.settingsManager, this.repoTracker);
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider('*', completionProvider, '@') // Trigger on '@' for inline completion
);
// Register the main command
let mainCommand = vscode.commands.registerCommand('C4B3Rstudios.93m1n1gpt', () => {
vscode.window.showInformationMessage('93m1n1gpt is now active!');

19
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@langchain/community": "^0.3.29",
"@langchain/core": "^0.3.39",
"@langchain/ollama": "^0.2.0",
"diff": "^7.0.0",
"highlight.js": "^11.11.1",
"markdown-it": "^14.1.0",
"marked": "^15.0.7"
@ -1751,10 +1752,10 @@
}
},
"node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"dev": true,
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
@ -3146,6 +3147,16 @@
"wrap-ansi": "^7.0.0"
}
},
"node_modules/mocha/node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/mocha/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",

View File

@ -24,6 +24,12 @@
"title": "93m1n1GPT: Show LLM Selector"
}
],
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": true
}
},
"viewsContainers": {
"activitybar": [
{
@ -76,6 +82,7 @@
"@langchain/community": "^0.3.29",
"@langchain/core": "^0.3.39",
"@langchain/ollama": "^0.2.0",
"diff": "^7.0.0",
"highlight.js": "^11.11.1",
"markdown-it": "^14.1.0",
"marked": "^15.0.7"

View File

@ -0,0 +1,63 @@
// ChatService.js
const vscode = require("vscode")
const { ChatOllama } = require("@langchain/ollama");
const { PromptTemplate } = require("@langchain/core/prompts");
class ChatService {
constructor(settingsManager) {
this._settingsManager = settingsManager;
}
async handleChatMessage(webviewView, userMessage, messageHistory, markdownRenderer, repoTracker) {
messageHistory.push({ role: "user", content: userMessage });
const config = this._getConfig();
const contextInfo = await this._getContextInfo(repoTracker);
const chat = this._createChatInstance(config);
await this._streamResponse(chat, messageHistory, webviewView, markdownRenderer);
}
_getConfig() {
const config = vscode.workspace.getConfiguration('93m1n1gpt');
return {
model: config.get('selectedModel') || "llama2:13b",
apiUrl: config.get('apiUrl') || "http://localhost:11434"
};
}
async _getContextInfo(repoTracker) {
const gitStatus = await repoTracker.getGitStatus();
return gitStatus ?
`\nCurrent git branch: ${gitStatus.branch}\nLast commit: ${gitStatus.lastCommit}` : '';
}
_createChatInstance(config) {
return new ChatOllama({
model: config.model,
baseUrl: config.apiUrl,
temperature: 0.6,
topP: 0.9,
topK: 40,
streaming: true
});
}
async _streamResponse(chat, messageHistory, webviewView, markdownRenderer) {
let currentStreamedText = "";
const stream = await chat.stream([...messageHistory]);
for await (const chunk of stream) {
currentStreamedText += chunk.content;
const formattedMarkdown = markdownRenderer.render(currentStreamedText);
webviewView.webview.postMessage({
command: "updateBotMessage",
html: formattedMarkdown
});
}
messageHistory.push({ role: "assistant", content: currentStreamedText });
}
}
module.exports = { ChatService };

View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ollama LLM Selection</title>
<!-- Include any necessary CSS frameworks or stylesheets here -->
</head>
<body>
<h1>Select an LLM</h1>
<p>Choose an LLM from the list below:</p>
<select id="llmSelector">
<!-- Options will be dynamically populated with available LLMs -->
</select>
<button id="selectButton">Select</button>
<script>
// Function to fetch available LLMs from Ollama
async function fetchAvailableLLMs() {
try {
const response = await fetch('https://api.ollama.ai/tags');
if (!response.ok) {
throw new Error('Failed to fetch available LLMs');
}
const data = await response.json();
return data.tags;
} catch (error) {
console.error('Error fetching available LLMs:', error);
return [];
}
}
// Function to populate the LLM selector with options
async function populateLLMSelector() {
const llms = await fetchAvailableLLMs();
const llmSelector = document.getElementById('llmSelector');
llms.forEach(llm => {
const option = document.createElement('option');
option.value = llm;
option.textContent = llm;
llmSelector.appendChild(option);
});
}
// Event listener for the "Select" button
document.getElementById('selectButton').addEventListener('click', () => {
const selectedLLM = document.getElementById('llmSelector').value;
// Here, you can perform actions based on the selected LLM, such as sending it to the extension.
console.log('Selected LLM:', selectedLLM);
// You can also send a message to the extension to set up the selected LLM.
});
// Call the function to populate the LLM selector
populateLLMSelector();
</script>
</body>
</html>

View File

@ -0,0 +1,389 @@
// LLMSelectorPanel.js
const vscode = require('vscode');
class LLMSelectorPanel {
constructor(extensionUri, settingsManager) {
this.extensionUri = extensionUri;
this.panel = null;
this.settingsManager = settingsManager;
}
async createOrShow(viewColumn) {
if (this.panel) {
this.panel.reveal(viewColumn);
} else {
this.panel = vscode.window.createWebviewPanel(
'C4B3Rstudios.93m1n1gpt.llmSelector',
'93m1n1GPT LLM Selector',
viewColumn,
{
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'media')]
}
);
this.panel.webview.html = await this.getWebviewContent(this.panel.webview);
this.panel.webview.onDidReceiveMessage(
async message => {
switch (message.command) {
case 'selectGeneralLLM':
await vscode.workspace.getConfiguration('93m1n1gpt').update(
'selectedModel',
message.model,
vscode.ConfigurationTarget.Global
);
vscode.window.showInformationMessage('Selected general LLM: ' + message.model);
break;
case 'selectCompletionLLM':
await vscode.workspace.getConfiguration('93m1n1gpt').update(
'completionModel',
message.model,
vscode.ConfigurationTarget.Global
);
vscode.window.showInformationMessage('Selected code completion LLM: ' + message.model);
break;
case 'updateApiUrl':
await this.settingsManager.setSecure('apiUrl', message.url);
vscode.window.showInformationMessage('API URL updated securely');
break;
}
}
);
this.panel.onDidDispose(() => {
this.panel = null;
});
}
}
async getWebviewContent(webview) {
try {
const apiUrl = await this.settingsManager.getSecure('apiUrl') || 'http://localhost:11434';
const config = vscode.workspace.getConfiguration('93m1n1gpt');
const generalModel = config.get('selectedModel') || 'codellama:7b';
const completionModel = config.get('completionModel') || 'codellama:7b';
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>93m1n1GPT LLM Selection</title>
<style>
body {
padding: 20px;
font-family: var(--vscode-font-family);
color: var(--vscode-foreground);
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.card {
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 20px;
margin-bottom: 20px;
}
h1 {
font-size: 24px;
font-weight: 600;
color: var(--vscode-foreground);
margin: 0 0 20px 0;
border-bottom: 1px solid var(--vscode-panel-border);
padding-bottom: 10px;
}
h2 {
font-size: 18px;
font-weight: 600;
color: var(--vscode-foreground);
margin: 20px 0 15px 0;
}
.input-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: var(--vscode-foreground);
font-weight: 500;
}
input[type="text"], select {
width: 100%;
padding: 8px 12px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
font-size: 14px;
margin-bottom: 10px;
}
input[type="text"]:focus, select:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
select {
height: 36px;
}
button {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background: var(--vscode-button-hoverBackground);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status {
margin-top: 10px;
padding: 10px;
border-radius: 4px;
font-size: 14px;
}
.status.error {
background: var(--vscode-inputValidation-errorBackground);
border: 1px solid var(--vscode-inputValidation-errorBorder);
color: var(--vscode-inputValidation-errorForeground);
}
.status.info {
background: var(--vscode-infoBackground);
border: 1px solid var(--vscode-infoBorder);
color: var(--vscode-infoForeground);
}
.button-container {
display: flex;
gap: 10px;
margin-top: 15px;
}
.model-count {
font-size: 12px;
color: var(--vscode-descriptionForeground);
margin-top: 5px;
}
.spinner {
border: 2px solid var(--vscode-input-background);
border-top: 2px solid var(--vscode-focusBorder);
border-radius: 50%;
width: 16px;
height: 16px;
animation: spin 1s linear infinite;
display: inline-block;
vertical-align: middle;
margin-right: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<h1>93m1n1GPT Configuration</h1>
<div class="input-group">
<label for="apiUrl">Ollama API URL</label>
<input type="text"
id="apiUrl"
value="${apiUrl}"
placeholder="Enter Ollama API URL"
/>
<button id="saveApiUrl">Save API URL</button>
</div>
<h2>LLM Model Selection</h2>
<div class="input-group">
<label for="generalLLMSelector">General Model</label>
<select id="generalLLMSelector">
<option value="">Loading available models...</option>
</select>
<div class="model-count" id="generalModelCount"></div>
</div>
<div class="input-group">
<label for="completionLLMSelector">Code Completion Model</label>
<select id="completionLLMSelector">
<option value="">Loading available models...</option>
</select>
<div class="model-count" id="completionModelCount"></div>
</div>
<div id="status" class="status" style="display: none;"></div>
<div class="button-container">
<button id="selectGeneralButton" disabled>Select General Model</button>
<button id="selectCompletionButton" disabled>Select Completion Model</button>
<button id="refreshModels">
<span id="refreshSpinner" class="spinner" style="display: none;"></span>
Refresh Models
</button>
</div>
</div>
</div>
<script>
const vscode = acquireVsCodeApi();
const generalLLMSelector = document.getElementById('generalLLMSelector');
const completionLLMSelector = document.getElementById('completionLLMSelector');
const selectGeneralButton = document.getElementById('selectGeneralButton');
const selectCompletionButton = document.getElementById('selectCompletionButton');
const statusDiv = document.getElementById('status');
const apiUrlInput = document.getElementById('apiUrl');
const saveApiUrlButton = document.getElementById('saveApiUrl');
const refreshButton = document.getElementById('refreshModels');
const refreshSpinner = document.getElementById('refreshSpinner');
const generalModelCountDiv = document.getElementById('generalModelCount');
const completionModelCountDiv = document.getElementById('completionModelCount');
async function fetchAvailableLLMs() {
try {
refreshSpinner.style.display = 'inline-block';
const apiUrl = apiUrlInput.value;
const response = await fetch(apiUrl + '/api/tags');
if (!response.ok) {
throw new Error('Failed to fetch available LLMs');
}
const data = await response.json();
return data.models || [];
} catch (error) {
console.error('Error fetching available LLMs:', error);
showError('Failed to fetch models. Please ensure Ollama is running.');
return [];
} finally {
refreshSpinner.style.display = 'none';
}
}
function showError(message) {
statusDiv.style.display = 'block';
statusDiv.className = 'status error';
statusDiv.textContent = message;
}
function showStatus(message) {
statusDiv.style.display = 'block';
statusDiv.className = 'status info';
statusDiv.textContent = message;
}
async function populateLLMSelectors() {
showStatus('Fetching available models...');
const llms = await fetchAvailableLLMs();
// Populate general LLM selector
generalLLMSelector.innerHTML = '';
completionLLMSelector.innerHTML = '';
if (llms.length === 0) {
const noModelsOption = '<option value="">No models found</option>';
generalLLMSelector.innerHTML = noModelsOption;
completionLLMSelector.innerHTML = noModelsOption;
selectGeneralButton.disabled = true;
selectCompletionButton.disabled = true;
generalModelCountDiv.textContent = 'No models available';
completionModelCountDiv.textContent = 'No models available';
return;
}
const defaultOption = '<option value="">Select a model</option>';
generalLLMSelector.innerHTML = defaultOption;
completionLLMSelector.innerHTML = defaultOption;
llms.forEach(llm => {
const option = '<option value="' + llm.name + '">' + llm.name + '</option>';
generalLLMSelector.innerHTML += option;
completionLLMSelector.innerHTML += option;
});
generalModelCountDiv.textContent = llms.length + ' model' + (llms.length === 1 ? '' : 's') + ' available';
completionModelCountDiv.textContent = llms.length + ' model' + (llms.length === 1 ? '' : 's') + ' available';
selectGeneralButton.disabled = true;
selectCompletionButton.disabled = true;
statusDiv.style.display = 'none';
}
generalLLMSelector.addEventListener('change', () => {
selectGeneralButton.disabled = !generalLLMSelector.value;
});
completionLLMSelector.addEventListener('change', () => {
selectCompletionButton.disabled = !completionLLMSelector.value;
});
selectGeneralButton.addEventListener('click', () => {
const selectedLLM = generalLLMSelector.value;
if (selectedLLM) {
vscode.postMessage({
command: 'selectGeneralLLM',
model: selectedLLM
});
}
});
selectCompletionButton.addEventListener('click', () => {
const selectedLLM = completionLLMSelector.value;
if (selectedLLM) {
vscode.postMessage({
command: 'selectCompletionLLM',
model: selectedLLM
});
}
});
saveApiUrlButton.addEventListener('click', async () => {
showStatus('Updating API URL...');
await vscode.postMessage({
command: 'updateApiUrl',
url: apiUrlInput.value
});
populateLLMSelectors();
});
refreshButton.addEventListener('click', () => {
populateLLMSelectors();
});
// Initialize the selectors
populateLLMSelectors();
</script>
</body>
</html>`;
} catch (error) {
console.error('Error generating webview content:', error);
return '<html><body>Error loading content: ' + error.message + '</body></html>';
}
}
}
module.exports = { LLMSelectorPanel };

View File

@ -0,0 +1,74 @@
const vscode = require('vscode');
class SecureSettingsManager {
constructor(context) {
this.secretStorage = context.secrets;
this.CONFIG_PREFIX = '93m1n1gpt.';
this.SECURE_KEYS = ['apiUrl']; // List of keys that should be stored securely
}
async initialize() {
// Migrate any existing settings to secure storage
const config = vscode.workspace.getConfiguration('93m1n1gpt');
for (const key of this.SECURE_KEYS) {
const existingValue = config.get(key);
if (existingValue) {
await this.setSecure(key, existingValue);
// Clear the value from regular configuration
await config.update(key, undefined, vscode.ConfigurationTarget.Global);
}
}
}
async getSecure(key) {
try {
const value = await this.secretStorage.get(this.CONFIG_PREFIX + key);
return value || null;
} catch (error) {
console.error(`Error retrieving secure setting ${key}:`, error);
return null;
}
}
async setSecure(key, value) {
try {
await this.secretStorage.store(this.CONFIG_PREFIX + key, value);
return true;
} catch (error) {
console.error(`Error storing secure setting ${key}:`, error);
return false;
}
}
async deleteSecure(key) {
try {
await this.secretStorage.delete(this.CONFIG_PREFIX + key);
return true;
} catch (error) {
console.error(`Error deleting secure setting ${key}:`, error);
return false;
}
}
// Helper method to get all settings (both secure and non-secure)
async getAllSettings() {
const config = vscode.workspace.getConfiguration('93m1n1gpt');
const settings = {};
// Get non-secure settings
for (const key of Object.keys(config)) {
if (!this.SECURE_KEYS.includes(key)) {
settings[key] = config.get(key);
}
}
// Get secure settings
for (const key of this.SECURE_KEYS) {
settings[key] = await this.getSecure(key);
}
return settings;
}
}
module.exports = SecureSettingsManager;

View File

@ -0,0 +1,60 @@
// AISidebarProvider.js
const { MarkdownRenderer } = require("../utils/MarkdownRender");
const { ChatService } = require("../handlers/ChatService");
const { FileSystemManager } = require("./FileSystemManager");
const { GitManager } = require("./GitManager");
const { UIManager } = require("./UIManager");
class AISidebarProvider {
constructor(extensionUri, settingsManager, repoTracker) {
this._extensionUri = extensionUri;
this._settingsManager = settingsManager;
this.repoTracker = repoTracker;
this.messageHistory = [];
this._disposables = [];
// Initialize services
this.markdownRenderer = new MarkdownRenderer();
this.chatService = new ChatService(settingsManager);
this.fileManager = new FileSystemManager(repoTracker);
this.gitManager = new GitManager(repoTracker);
this.uiManager = new UIManager(this._extensionUri);
}
async resolveWebviewView(webviewView, context, _token) {
this._view = webviewView;
this.uiManager.initializeWebview(webviewView, this._extensionUri);
await this._setupServices(webviewView);
this._setupMessageHandlers(webviewView);
}
async _setupServices(webviewView) {
await this.fileManager.setupFileWatcher(webviewView, this._disposables);
await this.gitManager.setupGitWatcher(webviewView, this._disposables);
}
_setupMessageHandlers(webviewView) {
webviewView.webview.onDidReceiveMessage(async (message) => {
const handlers = {
chat: () => this.chatService.handleChatMessage(webviewView, message.text, this.messageHistory, this.markdownRenderer, this.repoTracker),
copyCode: () => this.uiManager.handleCopyCode(message.text),
insertCode: () => this.uiManager.handleInsertCode(message.text),
openFile: () => this.fileManager.openFileInEditor(message.path, message.line),
getFileReferences: () => this.fileManager.handleFileReferenceRequest(webviewView, message.prefix),
getGitStatus: () => this.gitManager.sendGitStatus(webviewView)
};
const handler = handlers[message.command];
if (handler) {
await handler();
}
});
}
dispose() {
this._disposables.forEach(d => d.dispose());
}
}
module.exports = { AISidebarProvider };

View File

@ -0,0 +1,69 @@
// FileSystemManager.js
const vscode = require("vscode");
class FileSystemManager {
constructor(repoTracker) {
this.repoTracker = repoTracker;
}
async setupFileWatcher(webviewView, disposables) {
await this._sendFileTree(webviewView);
const watcher = vscode.workspace.createFileSystemWatcher('**/*');
['onDidChange', 'onDidCreate', 'onDidDelete'].forEach(event => {
watcher[event](() => this._sendFileTree(webviewView));
});
disposables.push(watcher);
}
async _sendFileTree(webviewView) {
const fileTree = await this.repoTracker.buildFileTree();
webviewView.webview.postMessage({
command: 'updateFileTree',
fileTree: fileTree
});
}
async handleFileReferenceRequest(webviewView, prefix) {
const fileTree = this.repoTracker.fileTree;
const files = this._flattenFileTree(fileTree)
.filter(file => file.path.toLowerCase().includes(prefix.toLowerCase()))
.slice(0, 10);
webviewView.webview.postMessage({
command: 'fileReferenceSuggestions',
suggestions: files
});
}
_flattenFileTree(tree, path = '') {
return tree.reduce((acc, item) => {
if (item.type === 'file') {
acc.push({ type: 'file', path: path + item.name, name: item.name });
} else if (item.type === 'directory' && item.children) {
acc.push(...this._flattenFileTree(item.children, path + item.name + '/'));
}
return acc;
}, []);
}
async openFileInEditor(filePath, line) {
try {
const document = await vscode.workspace.openTextDocument(filePath);
const editor = await vscode.window.showTextDocument(document);
if (line) {
const lineNumber = parseInt(line) - 1;
const range = editor.document.lineAt(lineNumber).range;
editor.selection = new vscode.Selection(range.start, range.end);
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
}
} catch (error) {
console.error('Error opening file:', error);
vscode.window.showErrorMessage(`Failed to open file: ${filePath}`);
}
}
}
module.exports = { FileSystemManager };

View File

@ -0,0 +1,25 @@
// GitManager.js
class GitManager {
constructor(repoTracker) {
this.repoTracker = repoTracker;
}
async setupGitWatcher(webviewView, disposables) {
await this.sendGitStatus(webviewView);
const gitWatcher = await this.repoTracker.watchGitChanges();
gitWatcher.onDidChange(() => this.sendGitStatus(webviewView));
disposables.push(gitWatcher);
}
async sendGitStatus(webviewView) {
const gitStatus = await this.repoTracker.getGitStatus();
webviewView.webview.postMessage({
command: 'updateGitStatus',
gitStatus: gitStatus
});
}
}
module.exports = { GitManager };

View File

@ -0,0 +1,48 @@
// MarkdownRenderer.js
const MarkdownIt = require("markdown-it");
const hljs = require("highlight.js");
class MarkdownRenderer {
constructor() {
this.md = new MarkdownIt({
highlight: this._highlightCode.bind(this),
html: true,
linkify: true
});
}
_highlightCode(str, lang) {
let highlighted;
try {
highlighted = lang && hljs.getLanguage(lang)
? hljs.highlight(str, { language: lang }).value
: this.md.utils.escapeHtml(str);
} catch (err) {
console.error("Syntax highlighting error:", err);
highlighted = this.md.utils.escapeHtml(str);
}
const escapedCode = this.md.utils.escapeHtml(str);
return this._wrapCodeBlock(escapedCode, highlighted, lang);
}
_wrapCodeBlock(escapedCode, highlighted, lang) {
return `
<div class="code-block-wrapper">
<div class="code-block-header">
<span class="code-language">${lang || 'text'}</span>
<div class="code-block-buttons">
<button class="code-button copy-button" data-code="${escapedCode}">Copy</button>
<button class="code-button insert-button" data-code="${escapedCode}">Insert</button>
</div>
</div>
<pre class="hljs"><code class="language-${lang || 'text'}">${highlighted}</code></pre>
</div>`;
}
render(text) {
return this.md.render(text);
}
}
module.exports = { MarkdownRenderer };

View File

@ -1,5 +1,7 @@
const vscode = require("vscode");
const { exec } = require('child_process');
const path = require('path')
const fs = require('fs')
const util = require('util');
const execPromise = util.promisify(exec);
@ -43,7 +45,7 @@ class RepoTracker {
await this.updateGitStatus();
} catch (error) {
console.log('Not a git repository or git not installed');
console.log('Not a git repository or git not installed: ', error);
this.gitInfo = null;
}
}
@ -158,7 +160,6 @@ class RepoTracker {
);
}
// ... (keep existing methods: buildFileTree, scanDirectory, getFileContent, getFileTreeSummary)
async buildFileTree() {
this.fileTree = await this.scanDirectory(this.workspaceRoot);
}

View File

@ -0,0 +1,64 @@
// UIManager.js
const vscode = require("vscode");
const { styles } = require("../webview/sidebar/styles");
const { scripts } = require("../webview/sidebar/scripts");
class UIManager {
constructor(extensionUri) {
this._extensionUri = extensionUri;
}
initializeWebview(webviewView, extensionUri) {
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [extensionUri]
};
webviewView.webview.html = this._getWebviewContent();
}
handleCopyCode(text) {
vscode.env.clipboard.writeText(text);
vscode.window.showInformationMessage('Code copied to clipboard!');
}
handleInsertCode(code) {
const editor = vscode.window.activeTextEditor;
if (editor) {
editor.edit(editBuilder => {
editBuilder.insert(editor.selection.active, code);
});
}
}
_getWebviewContent() {
return /*html*/`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>${styles}</style>
</head>
<body>
<div id="gitStatus"></div>
<div class="chat-container">
<div id="messageContainer" class="message-container"></div>
<div class="input-container">
<input
type="text"
id="promptInput"
placeholder="Type a message..."
class="prompt-input"
/>
<button id="sendButton" class="send-button">Send</button>
</div>
</div>
<script>${scripts}</script>
</body>
</html>
`;
}
}
module.exports = { UIManager };

View File

@ -0,0 +1,185 @@
// scripts.js
exports.scripts = /*javascript*/`
let fileTree = [];
let gitStatus = null;
const vscode = acquireVsCodeApi();
let messageHistory = [];
// Initialize elements after DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
const promptInput = document.getElementById('promptInput');
const sendButton = document.getElementById('sendButton');
// Send message on button click
sendButton.addEventListener('click', () => {
sendMessage();
});
// Send message on Enter key
promptInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Handle file reference suggestions
promptInput.addEventListener('input', (e) => {
const text = e.target.value;
const lastAtSign = text.lastIndexOf('@');
if (lastAtSign !== -1) {
const prefix = text.substring(lastAtSign + 1);
vscode.postMessage({
command: 'getFileReferences',
prefix: prefix
});
}
});
});
function sendMessage() {
const promptInput = document.getElementById('promptInput');
const text = promptInput.value.trim();
if (text) {
// Add user message to chat
addMessageToChat('user', text);
// Send to extension
vscode.postMessage({
command: 'chat',
text: text
});
// Clear input
promptInput.value = '';
}
}
function addMessageToChat(role, content) {
const messageContainer = document.getElementById('messageContainer');
const messageDiv = document.createElement('div');
messageDiv.className = \`message \${role}\`;
messageDiv.innerHTML = content;
messageContainer.appendChild(messageDiv);
messageContainer.scrollTop = messageContainer.scrollHeight;
// Store in history
messageHistory.push({ role, content });
}
// Handle messages from extension
window.addEventListener('message', event => {
const message = event.data;
switch (message.command) {
case 'updateGitStatus':
gitStatus = message.gitStatus;
updateGitStatusDisplay();
break;
case 'updateFileTree':
fileTree = message.fileTree;
break;
case 'fileReferenceSuggestions':
showFileReferenceSuggestions(message.suggestions);
break;
case 'updateBotMessage':
// Update last assistant message or create new one
updateBotMessage(message.html);
break;
}
});
function updateBotMessage(html) {
const messageContainer = document.getElementById('messageContainer');
let lastMessage = messageContainer.lastElementChild;
// If last message is not from assistant, create new one
if (!lastMessage || !lastMessage.classList.contains('assistant')) {
lastMessage = document.createElement('div');
lastMessage.className = 'message assistant';
messageContainer.appendChild(lastMessage);
}
lastMessage.innerHTML = html;
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function updateGitStatusDisplay() {
if (!gitStatus) return;
const statusEl = document.getElementById('gitStatus');
if (statusEl) {
statusEl.innerHTML = \`
<div class="git-info">
<span>Branch: \${gitStatus.branch}</span>
<span>Last commit: \${gitStatus.lastCommit}</span>
</div>
\`;
}
}
function showFileReferenceSuggestions(suggestions) {
let suggestionsDiv = document.getElementById('suggestions');
if (!suggestionsDiv) {
suggestionsDiv = document.createElement('div');
suggestionsDiv.id = 'suggestions';
suggestionsDiv.className = 'suggestions';
document.body.appendChild(suggestionsDiv);
}
if (suggestions.length === 0) {
suggestionsDiv.style.display = 'none';
return;
}
const promptInput = document.getElementById('promptInput');
const rect = promptInput.getBoundingClientRect();
suggestionsDiv.style.position = 'absolute';
suggestionsDiv.style.left = rect.left + 'px';
suggestionsDiv.style.top = (rect.bottom + 5) + 'px';
suggestionsDiv.style.display = 'block';
suggestionsDiv.innerHTML = suggestions.map(file => \`
<div class="suggestion" data-path="\${file.path}">
\${file.path}
</div>
\`).join('');
suggestionsDiv.querySelectorAll('.suggestion').forEach(el => {
el.addEventListener('click', () => {
const path = el.dataset.path;
const cursorPos = promptInput.selectionStart;
const textBeforeCursor = promptInput.value.substring(0, cursorPos);
const lastAtSign = textBeforeCursor.lastIndexOf('@');
const textAfterCursor = promptInput.value.substring(cursorPos);
promptInput.value = textBeforeCursor.substring(0, lastAtSign) +
'@' + path + ':' + textAfterCursor;
suggestionsDiv.style.display = 'none';
promptInput.focus();
});
});
}
// Handle code actions
document.addEventListener('click', (e) => {
if (e.target.classList.contains('copy-button')) {
vscode.postMessage({
command: 'copyCode',
text: e.target.dataset.code
});
} else if (e.target.classList.contains('insert-button')) {
vscode.postMessage({
command: 'insertCode',
text: e.target.dataset.code
});
}
// Hide suggestions when clicking outside
const suggestionsDiv = document.getElementById('suggestions');
if (suggestionsDiv && !suggestionsDiv.contains(e.target) && e.target !== document.getElementById('promptInput')) {
suggestionsDiv.style.display = 'none';
}
});
`;

View File

@ -0,0 +1,144 @@
// styles.js
exports.styles =/*css*/ `
body {
padding: 16px;
height: 100vh;
margin: 0;
display: flex;
flex-direction: column;
}
.git-info {
padding: 8px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: 4px;
margin-bottom: 8px;
font-size: 0.9em;
}
.git-info span {
margin-right: 16px;
color: var(--vscode-descriptionForeground);
}
.chat-container {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.message-container {
flex: 1;
overflow-y: auto;
margin-bottom: 16px;
padding: 8px;
}
.message {
margin-bottom: 16px;
padding: 8px;
border-radius: 4px;
background: var(--vscode-editor-inactiveSelectionBackground);
}
.message.user {
background: var(--vscode-editor-selectionBackground);
}
.input-container {
display: flex;
gap: 8px;
padding: 8px;
background: var(--vscode-editor-background);
border-top: 1px solid var(--vscode-panel-border);
}
.prompt-input {
flex: 1;
padding: 8px;
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
}
.send-button {
padding: 8px 16px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
}
.send-button:hover {
background: var(--vscode-button-hoverBackground);
}
.suggestions {
position: absolute;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.suggestion {
padding: 8px 12px;
cursor: pointer;
}
.suggestion:hover {
background: var(--vscode-list-hoverBackground);
}
.code-block-wrapper {
margin: 16px 0;
border-radius: 4px;
border: 1px solid var(--vscode-panel-border);
}
.code-block-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-bottom: 1px solid var(--vscode-panel-border);
}
.code-language {
font-family: var(--vscode-editor-font-family);
font-size: 0.9em;
color: var(--vscode-descriptionForeground);
}
.code-block-buttons {
display: flex;
gap: 8px;
}
.code-button {
padding: 4px 8px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.code-button:hover {
background: var(--vscode-button-hoverBackground);
}
pre.hljs {
margin: 0;
padding: 16px;
overflow-x: auto;
}
`;

View File

@ -0,0 +1,29 @@
const getBaseStyles = require('./styles');
const getBaseScripts = require('./scripts');
function getWebviewContent() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/vs2015.min.css">
<style>
${getBaseStyles()}
</style>
</head>
<body>
<div id="gitStatus"></div>
<div id="chatContainer"></div>
<div class="input-container">
<textarea id="prompt" placeholder="Type your message... Use @ to reference files"></textarea>
<button id="send">Send</button>
</div>
<script>
${getBaseScripts()}
</script>
</body>
</html>`;
}
module.exports = getWebviewContent;

View File

@ -1,4 +1,5 @@
// AISidebarProvider.js
const vscode = require("vscode");
const { MarkdownRenderer } = require("../utils/MarkdownRender");
const { ChatService } = require("../handlers/ChatService");
const { FileSystemManager } = require("./FileSystemManager");
@ -10,10 +11,11 @@ class AISidebarProvider {
this._extensionUri = extensionUri;
this._settingsManager = settingsManager;
this.repoTracker = repoTracker;
this.messageHistory = [];
this.chatSessions = new Map();
this.currentSessionId = null;
this._disposables = [];
this.currentContext = { files: [], git: true, history: true }; // Track file, Git, and history contexts
// Initialize services
this.markdownRenderer = new MarkdownRenderer();
this.chatService = new ChatService(settingsManager);
this.fileManager = new FileSystemManager(repoTracker);
@ -25,33 +27,186 @@ class AISidebarProvider {
this._view = webviewView;
this.uiManager.initializeWebview(webviewView, this._extensionUri);
console.log('Building file tree on initialization');
await this.repoTracker.buildFileTree();
console.log('File tree built:', this.repoTracker.fileTree.length, 'items');
await this._setupServices(webviewView);
this._setupMessageHandlers(webviewView);
this.updateContextDisplay(webviewView); // Initialize context display
}
async _setupServices(webviewView) {
await this.fileManager.setupFileWatcher(webviewView, this._disposables);
await this.gitManager.setupGitWatcher(webviewView, this._disposables);
try {
await this.fileManager.setupFileWatcher(webviewView, this._disposables);
await this.gitManager.setupGitWatcher(webviewView, this._disposables);
} catch (error) {
console.error('Service setup failed:', error.message);
webviewView.webview.postMessage({
command: 'error',
message: 'Service initialization failed: ' + error.message
});
}
}
_setupMessageHandlers(webviewView) {
webviewView.webview.onDidReceiveMessage(async (message) => {
console.log('Received message:', message.command, message);
const handlers = {
chat: () => this.chatService.handleChatMessage(webviewView, message.text, this.messageHistory, this.markdownRenderer, this.repoTracker),
chat: () => this.handleChatWithRAG(webviewView, message.text, false),
regenerate: () => this.handleChatWithRAG(webviewView, message.text, true),
copyCode: () => this.uiManager.handleCopyCode(message.text),
insertCode: () => this.uiManager.handleInsertCode(message.text),
openFile: () => this.fileManager.openFileInEditor(message.path, message.line),
getFileReferences: () => this.fileManager.handleFileReferenceRequest(webviewView, message.prefix),
getGitStatus: () => this.gitManager.sendGitStatus(webviewView)
getGitStatus: () => this.gitManager.sendGitStatus(webviewView),
getChatHistory: () => this.sendChatHistory(webviewView),
restoreChat: () => this.restoreChat(webviewView, message.sessionId),
addFileContext: () => this.addFileContext(webviewView, message.file),
removeContext: () => this.removeContext(webviewView, message.type, message.id)
};
const handler = handlers[message.command];
if (handler) {
await handler();
try {
await handler();
} catch (error) {
console.error(`Error handling ${message.command}:`, error.message);
webviewView.webview.postMessage({
command: 'error',
message: `Error processing ${message.command}: ${error.message}`
});
}
}
});
}
async handleChatWithRAG(webviewView, text, isRegenerate) {
if (!this.currentSessionId || !isRegenerate) {
this.currentSessionId = Date.now().toString();
this.chatSessions.set(this.currentSessionId, {
title: text.substring(0, 50),
messages: []
});
}
let session = this.chatSessions.get(this.currentSessionId);
let contextParts = [];
// Add file context
for (const fileContext of this.currentContext.files) {
const content = await this.fileManager.getFileContent(fileContext.path);
if (content) {
contextParts.push('File Context (' + fileContext.path + '):\n' + content);
}
}
// Add Git context if enabled
if (this.currentContext.git) {
let gitContext = '';
try {
gitContext = await this.gitManager.getRelevantGitContext(text || '');
} catch (error) {
gitContext = 'No git context available: ' + (error.message || 'Unknown error');
}
contextParts.push('Git Context:\n' + gitContext);
}
// Add chat history context if enabled
if (this.currentContext.history) {
let historyContext = this.findRelevantHistory(session.messages);
if (historyContext) {
contextParts.push('Chat History Context:\n' + historyContext);
}
}
const augmentedText = text + (text ? '\n\n' : '') + contextParts.join('\n\n');
try {
const response = await this.chatService.handleChatMessage(
webviewView,
augmentedText,
session.messages,
this.markdownRenderer,
this.repoTracker
);
if (response) {
session.messages.push({ role: 'user', content: text });
session.messages.push({ role: 'assistant', content: response });
}
} catch (error) {
webviewView.webview.postMessage({
command: 'error',
message: 'Chat error: ' + error.message
});
}
}
findRelevantHistory(messages) {
if (!messages || messages.length === 0) return '';
const keywords = (messages[messages.length - 1]?.content || '').toLowerCase().split(/\s+/);
return messages
.filter(msg => msg && msg.content && keywords.some(kw => msg.content.toLowerCase().includes(kw)))
.map(msg => '[' + msg.role + ']: ' + msg.content)
.join('\n');
}
sendChatHistory(webviewView) {
const historyTitles = Array.from(this.chatSessions.entries()).map(([id, session]) => ({
sessionId: id,
title: session.title
}));
webviewView.webview.postMessage({
command: 'showChatHistory',
history: historyTitles
});
}
restoreChat(webviewView, sessionId) {
if (this.chatSessions.has(sessionId)) {
this.currentSessionId = sessionId;
const session = this.chatSessions.get(sessionId);
webviewView.webview.postMessage({
command: 'restoreChat',
messages: session.messages
});
}
}
updateContextDisplay(webviewView) {
webviewView.webview.postMessage({
command: 'updateContext',
context: this.currentContext
});
}
addFileContext(webviewView, file) {
const fileExists = this.repoTracker.fileTree
.flat(Infinity)
.some(item => item.type === 'file' && item.path === file);
if (!fileExists) {
webviewView.webview.postMessage({
command: 'error',
message: 'File ' + file + ' not found in workspace'
});
return;
}
this.currentContext.files.push({ path: file });
this.updateContextDisplay(webviewView);
}
removeContext(webviewView, type, id) {
if (type === 'file' && id !== undefined) {
this.currentContext.files.splice(id, 1);
} else if (type === 'git') {
this.currentContext.git = false;
} else if (type === 'history') {
this.currentContext.history = false;
}
this.updateContextDisplay(webviewView);
}
dispose() {
this._disposables.forEach(d => d.dispose());
}

View File

@ -0,0 +1,91 @@
// ChatHistoryManager.js
const path = require('path');
const fs = require('fs/promises');
const { embedText } = require('./embeddings'); // You'll need to implement this
class ChatHistoryManager {
constructor(extensionContext) {
this.extensionContext = extensionContext;
this.historyPath = path.join(extensionContext.globalStoragePath, 'chat_history.json');
this.chatHistories = [];
this.embeddings = [];
}
async initialize() {
try {
await fs.mkdir(path.dirname(this.historyPath), { recursive: true });
const historyExists = await fs.access(this.historyPath)
.then(() => true)
.catch(() => false);
if (historyExists) {
const data = await fs.readFile(this.historyPath, 'utf-8');
this.chatHistories = JSON.parse(data);
// Generate embeddings for existing history
this.embeddings = await Promise.all(
this.chatHistories.map(chat =>
embedText(chat.messages.map(m => m.content).join(' '))
)
);
}
} catch (error) {
console.error('Error initializing chat history:', error);
this.chatHistories = [];
this.embeddings = [];
}
}
async saveChat(messages) {
const chatSession = {
id: Date.now().toString(),
timestamp: new Date().toISOString(),
messages: messages,
title: this.generateChatTitle(messages)
};
this.chatHistories.push(chatSession);
const embedding = await embedText(messages.map(m => m.content).join(' '));
this.embeddings.push(embedding);
await this.persistHistory();
return chatSession;
}
generateChatTitle(messages) {
// Generate a title based on the first message or a summary
const firstMessage = messages[0]?.content || '';
return firstMessage.slice(0, 50) + (firstMessage.length > 50 ? '...' : '');
}
async persistHistory() {
try {
await fs.writeFile(
this.historyPath,
JSON.stringify(this.chatHistories, null, 2)
);
} catch (error) {
console.error('Error saving chat history:', error);
}
}
async searchHistory(query) {
const queryEmbedding = await embedText(query);
const similarities = this.embeddings.map((embedding, index) => ({
similarity: this.cosineSimilarity(queryEmbedding, embedding),
chat: this.chatHistories[index]
}));
return similarities
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 5)
.map(item => item.chat);
}
cosineSimilarity(vecA, vecB) {
const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0);
const magA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));
const magB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));
return dotProduct / (magA * magB);
}
}
module.exports = {ChatHistoryManager}

View File

@ -0,0 +1,69 @@
// CodeCompletionProvider.js
const vscode = require('vscode');
class CodeCompletionProvider {
constructor(settingsManager, repoTracker) {
this.settingsManager = settingsManager;
this.repoTracker = repoTracker;
}
async provideCompletionItems(document, position, token, context) {
try {
const config = vscode.workspace.getConfiguration('93m1n1gpt');
const completionModel = config.get('completionModel') || 'codellama:7b'; // Use completionModel
const apiUrl = config.get('apiUrl') || 'http://localhost:11434';
// Get the text before and after the cursor for context
const line = document.lineAt(position.line).text;
const prefix = line.substring(0, position.character);
const triggerChar = context.triggerCharacter;
if (triggerChar !== '@') return []; // Only trigger on '@'
// Prepare prompt for Ollama, including context from the current line and document
const prompt = `Complete the following code:\n\`\`\`${document.languageId}\n${prefix}\n\`\`\`\nProvide a single line completion suggestion that fits the context.`;
const response = await this.fetchCompletionFromOllama(prompt, completionModel, apiUrl);
if (!response || !response.response) return [];
const completion = new vscode.CompletionItem(response.response.trim(), vscode.CompletionItemKind.Text);
completion.insertText = response.response.trim(); // Insert the completion text
completion.range = new vscode.Range(position, position); // Replace only the '@' trigger
completion.documentation = new vscode.MarkdownString(`Suggested by ${completionModel} via Ollama`);
return [completion];
} catch (error) {
console.error('Error providing code completion:', error);
vscode.window.showErrorMessage('Failed to fetch code completion: ' + error.message);
return [];
}
}
async fetchCompletionFromOllama(prompt, model, apiUrl) {
try {
const response = await fetch(`${apiUrl}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: model,
prompt: prompt,
stream: false,
options: { temperature: 0.7, top_k: 40, top_p: 0.9 } // Adjust for better code completion
})
});
if (!response.ok) {
throw new Error('Ollama API request failed: ' + response.statusText);
}
const data = await response.json();
return { response: data.response || data.text || '' }; // Adjust based on Ollama response format
} catch (error) {
throw new Error('Ollama API error: ' + error.message);
}
}
}
module.exports = { CodeCompletionProvider };

View File

@ -7,6 +7,7 @@ class FileSystemManager {
}
async setupFileWatcher(webviewView, disposables) {
console.log('Setting up file watcher');
await this._sendFileTree(webviewView);
const watcher = vscode.workspace.createFileSystemWatcher('**/*');
@ -18,6 +19,7 @@ class FileSystemManager {
}
async _sendFileTree(webviewView) {
console.log('Sending file tree update');
const fileTree = await this.repoTracker.buildFileTree();
webviewView.webview.postMessage({
command: 'updateFileTree',
@ -26,23 +28,36 @@ class FileSystemManager {
}
async handleFileReferenceRequest(webviewView, prefix) {
const fileTree = this.repoTracker.fileTree;
const files = this._flattenFileTree(fileTree)
.filter(file => file.path.toLowerCase().includes(prefix.toLowerCase()))
.slice(0, 10);
console.log('Handling file reference request for prefix:', prefix, ' - Checking repoTracker.fileTree:', !!this.repoTracker.fileTree);
if (!this.repoTracker.fileTree) {
console.error('repoTracker.fileTree is undefined or null');
webviewView.webview.postMessage({
command: 'fileReferenceSuggestions',
suggestions: []
});
return;
}
const suggestions = this._flattenFileTree(this.repoTracker.fileTree)
.filter(item => item.path.toLowerCase().startsWith(prefix.toLowerCase()))
.slice(0, 20); // Limit to 20 suggestions for performance
console.log('File and directory suggestions found:', suggestions.length);
webviewView.webview.postMessage({
command: 'fileReferenceSuggestions',
suggestions: files
suggestions
});
}
_flattenFileTree(tree, path = '') {
console.log('Flattening file tree at path:', path, ' - Tree items:', tree.length);
return tree.reduce((acc, item) => {
const fullPath = path + item.name + (item.type === 'directory' ? '/' : '');
if (item.type === 'file') {
acc.push({ type: 'file', path: path + item.name, name: item.name });
acc.push({ type: 'file', path: fullPath, name: item.name });
} else if (item.type === 'directory' && item.children) {
acc.push(...this._flattenFileTree(item.children, path + item.name + '/'));
acc.push({ type: 'directory', path: fullPath, name: item.name });
acc.push(...this._flattenFileTree(item.children, fullPath));
}
return acc;
}, []);
@ -50,6 +65,7 @@ class FileSystemManager {
async openFileInEditor(filePath, line) {
try {
console.log('Opening file:', filePath, 'at line:', line);
const document = await vscode.workspace.openTextDocument(filePath);
const editor = await vscode.window.showTextDocument(document);
@ -61,9 +77,9 @@ class FileSystemManager {
}
} catch (error) {
console.error('Error opening file:', error);
vscode.window.showErrorMessage(`Failed to open file: ${filePath}`);
vscode.window.showErrorMessage('Failed to open file: ' + filePath);
}
}
}
module.exports = { FileSystemManager };
module.exports = { FileSystemManager };

View File

@ -1,25 +1,62 @@
// GitManager.js
const vscode = require('vscode');
class GitManager {
constructor(repoTracker) {
this.repoTracker = repoTracker;
this.gitStatus = null;
this.gitWatcherDisposable = null;
}
async setupGitWatcher(webviewView, disposables) {
await this.sendGitStatus(webviewView);
const gitWatcher = await this.repoTracker.watchGitChanges();
gitWatcher.onDidChange(() => this.sendGitStatus(webviewView));
disposables.push(gitWatcher);
console.log('Setting up Git watcher');
try {
this.watchGitChanges(webviewView);
disposables.push(this.gitWatcherDisposable); // Ensure cleanup
} catch (error) {
console.error('Git watcher setup failed:', error.message);
webviewView.webview.postMessage({
command: 'error',
message: 'Git watcher initialization failed: ' + error.message
});
}
}
watchGitChanges(webviewView) {
console.log('Watching Git changes');
this.gitWatcherDisposable = this.repoTracker.watchGitChanges((gitStatus) => {
this.gitStatus = gitStatus;
webviewView.webview.postMessage({
command: 'updateGitStatus',
gitStatus: gitStatus
});
});
}
async getRelevantGitContext(query) {
console.log('Getting relevant Git context for query:', query);
const gitStatus = await this.repoTracker.getGitStatus();
if (!gitStatus) {
return 'No Git context available';
}
return 'Git Status - Branch: ' + gitStatus.branch + ', Last Commit: ' + gitStatus.lastCommit +
(gitStatus.hasChanges ? ', Changes: Yes' : ', Changes: No');
}
async sendGitStatus(webviewView) {
console.log('Sending Git status');
const gitStatus = await this.repoTracker.getGitStatus();
webviewView.webview.postMessage({
command: 'updateGitStatus',
gitStatus: gitStatus
gitStatus: gitStatus || { branch: 'unknown', lastCommit: 'No status available', hasChanges: false }
});
}
dispose() {
if (this.gitWatcherDisposable) {
this.gitWatcherDisposable.dispose();
}
}
}
module.exports = { GitManager };
module.exports = { GitManager };

View File

@ -1,231 +1,142 @@
const vscode = require("vscode");
const { exec } = require('child_process');
const path = require('path')
const fs = require('fs')
const util = require('util');
const execPromise = util.promisify(exec);
// RepoTracker.js
const vscode = require('vscode');
const fs = require('fs').promises;
const path = require('path');
class RepoTracker {
constructor() {
this.workspaceRoot = "";
this.workspaceRoot = vscode.workspace.rootPath || '';
this.fileTree = [];
this.gitInfo = null;
this.ignoredPaths = [
'node_modules',
'.git',
'dist',
'build',
'.next',
'.cache'
];
this._initializeCompletionProvider();
this.gitApi = null;
}
async initialize() {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) return;
this.workspaceRoot = workspaceFolders[0].uri.fsPath;
console.log('Initializing RepoTracker with workspace root:', this.workspaceRoot);
await this.buildFileTree();
await this.initializeGitTracking();
await this.initializeGitApi();
}
async initializeGitTracking() {
async initializeGitApi() {
try {
const { stdout: gitRoot } = await execPromise('git rev-parse --show-toplevel', {
cwd: this.workspaceRoot
});
this.gitInfo = {
root: gitRoot.trim(),
branch: '',
lastCommit: '',
status: []
};
await this.updateGitStatus();
this.gitApi = await vscode.git.getGitApi();
if (!this.gitApi) {
console.warn('Git API not available - Git may not be installed or enabled in workspace');
return;
}
console.log('Git API initialized for workspace:', this.workspaceRoot);
} catch (error) {
console.log('Not a git repository or git not installed: ', error);
this.gitInfo = null;
console.error('Error initializing Git API:', error);
}
}
async updateGitStatus() {
if (!this.gitInfo) return;
try {
// Get current branch
const { stdout: branch } = await execPromise('git branch --show-current', {
cwd: this.workspaceRoot
});
this.gitInfo.branch = branch.trim();
// Get last commit
const { stdout: lastCommit } = await execPromise('git log -1 --oneline', {
cwd: this.workspaceRoot
});
this.gitInfo.lastCommit = lastCommit.trim();
// Get status
const { stdout: status } = await execPromise('git status --porcelain', {
cwd: this.workspaceRoot
});
this.gitInfo.status = status.split('\n')
.filter(line => line.trim())
.map(line => ({
status: line.substring(0, 2).trim(),
file: line.substring(3).trim()
}));
} catch (error) {
console.error('Error updating git status:', error);
}
}
_initializeCompletionProvider() {
// Register the @ completion provider
vscode.languages.registerCompletionItemProvider(
{ pattern: '**' },
{
provideCompletionItems: (document, position) => {
const linePrefix = document.lineAt(position).text.substring(0, position.character);
// Check if we're typing after an @
if (!linePrefix.endsWith('@')) {
return undefined;
}
// Create completion items for all files in the workspace
return this.fileTree
.flat(Infinity)
.filter(item => item.type === 'file')
.map(file => {
const completionItem = new vscode.CompletionItem(
file.path,
vscode.CompletionItemKind.File
);
// When selected, it will insert the file path and prepare for line number
completionItem.insertText = file.path + ':';
completionItem.detail = `File: ${file.path}`;
completionItem.command = {
command: 'editor.action.triggerSuggest',
title: 'Re-trigger completions'
};
return completionItem;
});
}
},
'@' // Trigger character
);
// Register the line number completion provider
vscode.languages.registerCompletionItemProvider(
{ pattern: '**' },
{
provideCompletionItems: async (document, position) => {
const linePrefix = document.lineAt(position).text.substring(0, position.character);
// Check if we're typing after a file path and colon
const match = linePrefix.match(/@([^:]+):$/);
if (!match) {
return undefined;
}
const filePath = match[1];
const content = await this.getFileContent(filePath);
if (!content) {
return undefined;
}
// Create completion items for each line number with preview
const lines = content.split('\n');
return lines.map((line, index) => {
const lineNum = index + 1;
const completionItem = new vscode.CompletionItem(
`${lineNum}`,
vscode.CompletionItemKind.Reference
);
completionItem.detail = line.trim();
completionItem.documentation = new vscode.MarkdownString(
`Preview of line ${lineNum}:\n\`\`\`\n${line.trim()}\n\`\`\``
);
return completionItem;
});
}
},
':' // Trigger character
);
}
async buildFileTree() {
this.fileTree = await this.scanDirectory(this.workspaceRoot);
console.log('Building file tree for:', this.workspaceRoot);
if (!this.workspaceRoot) {
console.error('Workspace root not found');
this.fileTree = [];
return [];
}
try {
this.fileTree = await this._scanDirectory(this.workspaceRoot);
console.log('File tree built with:', this.fileTree.length, 'items');
return this.fileTree;
} catch (error) {
console.error('Error building file tree:', error);
this.fileTree = [];
return [];
}
}
async scanDirectory(dirPath) {
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
const files = [];
async _scanDirectory(dirPath) {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const tree = [];
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
const relativePath = path.relative(this.workspaceRoot, fullPath);
if (this.ignoredPaths.some(ignored => relativePath.includes(ignored))) {
continue;
}
if (entry.isDirectory()) {
const subFiles = await this.scanDirectory(fullPath);
files.push({
type: 'directory',
name: entry.name,
path: relativePath,
children: subFiles
});
} else {
files.push({
type: 'file',
name: entry.name,
path: relativePath,
extension: path.extname(entry.name)
});
const children = await this._scanDirectory(fullPath);
tree.push({ type: 'directory', path: relativePath, name: entry.name, children });
} else if (entry.isFile()) {
tree.push({ type: 'file', path: relativePath, name: entry.name });
}
}
return files;
return tree;
}
async getFileContent(filePath) {
const fullPath = path.join(this.workspaceRoot, filePath);
try {
const content = await fs.promises.readFile(fullPath, 'utf-8');
return content;
return await fs.readFile(fullPath, 'utf-8');
} catch (error) {
console.error(`Error reading file ${filePath}:`, error);
console.error('Error reading file:', error);
return null;
}
}
getFileTreeSummary() {
return JSON.stringify(this.fileTree, null, 2);
}
getGitStatus() {
return this.gitInfo;
async getGitStatus() {
if (!this.gitApi) {
console.warn('Git API not initialized');
return { branch: 'unknown', lastCommit: 'No Git status available' };
}
try {
const repository = this.gitApi.getRepository(this.workspaceRoot);
if (!repository) {
console.warn('No Git repository found for:', this.workspaceRoot);
return { branch: 'unknown', lastCommit: 'No Git repository' };
}
const status = await repository.status();
const branch = repository.state.HEAD?.name || 'unknown';
let lastCommit = 'No commits';
if (status) {
const log = await repository.log({ maxEntries: 1 });
lastCommit = log.length > 0 ? log[0].message : 'No commits';
}
return {
branch: branch,
lastCommit: lastCommit,
hasChanges: status ? status.workingTreeChanges.length > 0 || status.indexChanges.length > 0 : false
};
} catch (error) {
console.error('Error getting Git status:', error);
return { branch: 'unknown', lastCommit: 'Error fetching Git status', hasChanges: false };
}
}
async watchGitChanges() {
// Set up a file system watcher for git changes
const gitWatcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(this.workspaceRoot, '.git/**')
);
watchGitChanges(callback) {
if (!this.gitApi) {
console.warn('Git API not initialized - cannot watch Git changes');
return;
}
gitWatcher.onDidChange(() => this.updateGitStatus());
gitWatcher.onDidCreate(() => this.updateGitStatus());
gitWatcher.onDidDelete(() => this.updateGitStatus());
const repository = this.gitApi.getRepository(this.workspaceRoot);
if (!repository) {
console.warn('No Git repository found for watching changes');
return;
}
return gitWatcher;
console.log('Setting up Git changes watcher for workspace:', this.workspaceRoot);
const updateStatus = async () => {
const gitStatus = await this.getGitStatus();
callback(gitStatus);
};
const statusChangeDisposable = repository.state.onDidChange(() => updateStatus());
const repositoryChangeDisposable = this.gitApi.onDidOpenRepository(() => updateStatus());
const repositoryCloseDisposable = this.gitApi.onDidCloseRepository(() => callback({ branch: 'unknown', lastCommit: 'Repository closed', hasChanges: false }));
return {
dispose: () => {
statusChangeDisposable.dispose();
repositoryChangeDisposable.dispose();
repositoryCloseDisposable.dispose();
}
};
}
}

View File

@ -32,15 +32,17 @@ class UIManager {
}
_getWebviewContent() {
return `
return /*html*/`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>${styles}</style>
</head>
<body>
<div id="gitStatus"></div>
<div id="chatContainer" class="chat-container">
<div class="chat-container">
<div id="messageContainer" class="message-container"></div>
<div class="input-container">
<input

View File

@ -3,21 +3,86 @@ exports.scripts = /*javascript*/`
let fileTree = [];
let gitStatus = null;
const vscode = acquireVsCodeApi();
let currentContext = { files: [], git: true, history: true }; // Track file, Git, and history contexts
// Handle file reference suggestions
promptInput.addEventListener('input', (e) => {
const text = e.target.value;
const lastAtSign = text.lastIndexOf('@');
if (lastAtSign !== -1) {
const prefix = text.substring(lastAtSign + 1);
vscode.postMessage({
command: 'getFileReferences',
prefix: prefix
});
}
document.addEventListener('DOMContentLoaded', () => {
const promptInput = document.getElementById('promptInput');
const sendButton = document.getElementById('sendButton');
const messageContainer = document.getElementById('messageContainer');
const historyButton = document.createElement('button');
historyButton.textContent = 'Chat History';
historyButton.className = 'history-button';
historyButton.addEventListener('click', () => {
console.log('Chat history button clicked');
vscode.postMessage({ command: 'getChatHistory' });
});
messageContainer.insertAdjacentElement('beforebegin', historyButton);
const contextContainer = document.createElement('div');
contextContainer.id = 'contextContainer';
messageContainer.insertAdjacentElement('beforebegin', contextContainer);
updateContextDisplay(); // Initialize context display
sendButton.addEventListener('click', () => {
sendMessage();
});
promptInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
console.log('Key pressed:', e.key);
});
promptInput.addEventListener('input', debounce((e) => {
const text = e.target.value;
const lastAtSign = text.lastIndexOf('@');
console.log('Input changed:', text, 'Last @ at:', lastAtSign);
if (lastAtSign !== -1) {
console.log('Requesting file references for:', text);
vscode.postMessage({
command: 'getFileReferences',
prefix: text.substring(lastAtSign + 1)
});
} else {
console.log('Hiding suggestions due to no @');
hideSuggestions();
}
}, 300));
});
// Handle git status updates
function sendMessage() {
const promptInput = document.getElementById('promptInput');
const text = promptInput.value.trim();
if (text) {
addMessageToChat('user', text);
vscode.postMessage({
command: 'chat',
text: text
});
promptInput.value = '';
hideSuggestions();
}
}
function addMessageToChat(role, content) {
const messageContainer = document.getElementById('messageContainer');
const messageDiv = document.createElement('div');
messageDiv.className = 'message ' + role;
messageDiv.innerHTML = content;
messageContainer.appendChild(messageDiv);
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function clearChat() {
const messageContainer = document.getElementById('messageContainer');
messageContainer.innerHTML = '';
}
window.addEventListener('message', event => {
const message = event.data;
switch (message.command) {
@ -29,79 +94,293 @@ exports.scripts = /*javascript*/`
fileTree = message.fileTree;
break;
case 'fileReferenceSuggestions':
console.log('Received file reference suggestions:', message.suggestions.length, 'items');
showFileReferenceSuggestions(message.suggestions);
break;
case 'updateBotMessage':
updateChatDisplay(message.html);
updateBotMessage(message.html);
break;
case 'showChatHistory':
console.log('Showing chat history:', message.history.length);
showChatHistory(message.history);
break;
case 'restoreChat':
console.log('Restoring chat with', message.messages.length, 'messages');
restoreChat(message.messages);
break;
case 'error':
console.log('Error received:', message.message);
showError(message.message);
break;
case 'updateContext':
console.log('Updating context display with:', message.context);
currentContext = message.context;
updateContextDisplay();
break;
}
});
function updateContextDisplay() {
const contextContainer = document.getElementById('contextContainer');
contextContainer.innerHTML = '<h3>Current Context:</h3>';
currentContext.files.forEach((fileContext, index) => {
const div = document.createElement('div');
div.className = 'context-item';
div.innerHTML = fileContext.path + (fileContext.range ? ':' + fileContext.range.join('-') : '');
const removeBtn = document.createElement('button');
removeBtn.textContent = 'Remove';
removeBtn.className = 'remove-context';
removeBtn.onclick = () => {
console.log('Removing file context:', index);
currentContext.files.splice(index, 1);
updateContextDisplay();
vscode.postMessage({ command: 'removeContext', type: 'file', id: index });
};
div.appendChild(removeBtn);
contextContainer.appendChild(div);
});
if (currentContext.git) {
const div = document.createElement('div');
div.className = 'context-item';
div.innerHTML = 'Git Context';
const removeBtn = document.createElement('button');
removeBtn.textContent = 'Remove';
removeBtn.className = 'remove-context';
removeBtn.onclick = () => {
console.log('Removing git context');
currentContext.git = false;
updateContextDisplay();
vscode.postMessage({ command: 'removeContext', type: 'git' });
};
div.appendChild(removeBtn);
contextContainer.appendChild(div);
}
if (currentContext.history) {
const div = document.createElement('div');
div.className = 'context-item';
div.innerHTML = 'Chat History Context';
const removeBtn = document.createElement('button');
removeBtn.textContent = 'Remove';
removeBtn.className = 'remove-context';
removeBtn.onclick = () => {
console.log('Removing history context');
currentContext.history = false;
updateContextDisplay();
vscode.postMessage({ command: 'removeContext', type: 'history' });
};
div.appendChild(removeBtn);
contextContainer.appendChild(div);
}
}
function showFileReferenceSuggestions(suggestions) {
console.log('Attempting to show file reference suggestions');
const promptInput = document.getElementById('promptInput');
const messageContainer = document.getElementById('messageContainer');
if (!promptInput || !messageContainer) {
console.error('Prompt input or message container not found');
return;
}
const rect = promptInput.getBoundingClientRect();
let existingSuggestions = document.getElementById('suggestions');
if (existingSuggestions) {
existingSuggestions.remove();
console.log('Removed existing suggestions');
}
const suggestionsDiv = document.createElement('div');
suggestionsDiv.id = 'suggestions';
document.body.appendChild(suggestionsDiv);
if (suggestions.length === 0) {
suggestionsDiv.style.display = 'none';
console.log('No suggestions to show');
return;
}
// Group suggestions by directory structure for display
const groupedSuggestions = {};
suggestions.forEach(suggestion => {
const parts = suggestion.path.split('/');
let currentPath = '';
for (let i = 0; i < parts.length; i++) {
currentPath = i === 0 ? parts[i] : currentPath + '/' + parts[i];
if (i === parts.length - 1 && suggestion.type === 'file') {
groupedSuggestions[currentPath] = { path: suggestion.path, type: 'file' };
} else if (suggestion.type === 'directory' && i < parts.length - 1) {
groupedSuggestions[currentPath] = { path: currentPath, type: 'directory' };
}
}
});
// Render suggestions hierarchically using plain strings
let html = '';
Object.values(groupedSuggestions).forEach(item => {
const indent = item.path.split('/').length - 1;
html += '<div class="suggestion" data-path="' + item.path + '" data-type="' + item.type + '" style="padding-left: ' + (indent * 10) + 'px;">' +
item.path + ' (' + item.type + ')</div>';
});
suggestionsDiv.innerHTML = html;
const popupHeight = suggestionsDiv.offsetHeight || 200;
let topPosition = rect.top - popupHeight;
if (topPosition < 0) {
topPosition = rect.bottom + 5;
console.log('Adjusted suggestions to below input due to top overflow');
}
console.log('Positioning suggestions at:', { left: rect.left, top: topPosition });
suggestionsDiv.style.position = 'absolute';
suggestionsDiv.style.left = rect.left + 'px';
suggestionsDiv.style.top = topPosition + 'px';
suggestionsDiv.style.width = '300px';
suggestionsDiv.style.maxHeight = '200px';
suggestionsDiv.style.overflowY = 'auto';
suggestionsDiv.style.background = '#1e1e1e';
suggestionsDiv.style.border = '1px solid #444';
suggestionsDiv.style.color = '#d4d4d4';
suggestionsDiv.style.boxShadow = '0 2px 5px rgba(0,0,0,0.5)';
suggestionsDiv.style.zIndex = '10000';
suggestionsDiv.style.display = 'block';
console.log('Suggestions HTML set, items:', suggestions.length);
const popupBottom = topPosition + popupHeight;
const viewportHeight = window.innerHeight;
if (popupBottom > viewportHeight) {
const scrollAmount = popupBottom - viewportHeight + 10;
console.log('Scrolling down by:', scrollAmount);
messageContainer.scrollTop += scrollAmount;
}
suggestionsDiv.querySelectorAll('.suggestion').forEach(el => {
if (el.dataset.type === 'file') { // Only allow clicking files, not directories
el.addEventListener('click', () => {
const path = el.dataset.path;
const cursorPos = promptInput.selectionStart;
const textBeforeCursor = promptInput.value.substring(0, cursorPos);
const lastAtSign = textBeforeCursor.lastIndexOf('@');
const textAfterCursor = promptInput.value.substring(cursorPos);
// Remove the @prefix and insert only the path followed by a colon
promptInput.value = textBeforeCursor.substring(0, lastAtSign) + '@' + path + ':' + textAfterCursor;
// Add to context
currentContext.files.push({ path: path });
updateContextDisplay();
vscode.postMessage({
command: 'addFileContext',
file: path
});
// Close the popup
suggestionsDiv.style.display = 'none';
promptInput.focus();
});
} else {
el.style.cursor = 'default'; // Indicate directories are not clickable
el.style.opacity = '0.7'; // Dim directories to distinguish from files
}
});
setTimeout(() => {
document.addEventListener('click', function hideSuggestions(e) {
if (!suggestionsDiv.contains(e.target) && e.target !== promptInput) {
console.log('Hiding suggestions due to click outside');
suggestionsDiv.style.display = 'none';
document.removeEventListener('click', hideSuggestions);
}
});
}, 200);
}
function updateBotMessage(html) {
const messageContainer = document.getElementById('messageContainer');
let lastMessage = messageContainer.lastElementChild;
if (!lastMessage || !lastMessage.classList.contains('assistant')) {
lastMessage = document.createElement('div');
lastMessage.className = 'message assistant';
messageContainer.appendChild(lastMessage);
}
lastMessage.innerHTML = html;
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function updateGitStatusDisplay() {
if (!gitStatus) return;
const statusEl = document.getElementById('gitStatus');
if (statusEl) {
statusEl.innerHTML = \`
<div class="git-info">
<span>Branch: \${gitStatus.branch}</span>
<span>Last commit: \${gitStatus.lastCommit}</span>
</div>
\`;
statusEl.innerHTML = '<div class="git-info"><span>Branch: ' + gitStatus.branch + '</span><span>Last commit: ' + gitStatus.lastCommit + '</span></div>';
}
}
function showFileReferenceSuggestions(suggestions) {
let suggestionsDiv = document.getElementById('suggestions');
if (!suggestionsDiv) {
suggestionsDiv = document.createElement('div');
suggestionsDiv.id = 'suggestions';
suggestionsDiv.className = 'suggestions';
document.body.appendChild(suggestionsDiv);
}
if (suggestions.length === 0) {
function hideSuggestions() {
const suggestionsDiv = document.getElementById('suggestions');
if (suggestionsDiv) {
suggestionsDiv.style.display = 'none';
return;
console.log('Hid suggestions');
}
}
const rect = promptInput.getBoundingClientRect();
suggestionsDiv.style.position = 'absolute';
suggestionsDiv.style.left = rect.left + 'px';
suggestionsDiv.style.top = (rect.bottom + 5) + 'px';
suggestionsDiv.style.display = 'block';
suggestionsDiv.innerHTML = suggestions.map(file => \`
<div class="suggestion" data-path="\${file.path}">
\${file.path}
</div>
\`).join('');
suggestionsDiv.querySelectorAll('.suggestion').forEach(el => {
el.addEventListener('click', () => {
const path = el.dataset.path;
const cursorPos = promptInput.selectionStart;
const textBeforeCursor = promptInput.value.substring(0, cursorPos);
const lastAtSign = textBeforeCursor.lastIndexOf('@');
const textAfterCursor = promptInput.value.substring(cursorPos);
promptInput.value = textBeforeCursor.substring(0, lastAtSign) +
'@' + path + ':' + textAfterCursor;
suggestionsDiv.style.display = 'none';
promptInput.focus();
function showChatHistory(history) {
const historyModal = document.createElement('div');
historyModal.className = 'history-modal';
historyModal.innerHTML = '<div class="history-content"><h2>Chat History</h2><div class="history-messages">' +
history.map(session => '<div class="history-item" data-session-id="' + session.sessionId + '">' + session.title + '</div>').join('') +
'</div><button class="close-history">Close</button></div>';
document.body.appendChild(historyModal);
historyModal.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', () => {
const sessionId = item.dataset.sessionId;
vscode.postMessage({
command: 'restoreChat',
sessionId: sessionId
});
historyModal.remove();
});
});
historyModal.querySelector('.close-history').addEventListener('click', () => {
historyModal.remove();
});
}
function updateChatDisplay(html) {
const chatContainer = document.getElementById('chatContainer');
if (chatContainer) {
chatContainer.innerHTML = html;
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function restoreChat(messages) {
clearChat();
messages.forEach(msg => addMessageToChat(msg.role, msg.content));
}
function showError(errorMessage) {
const messageContainer = document.getElementById('messageContainer');
const errorDiv = document.createElement('div');
errorDiv.className = 'message error';
errorDiv.textContent = 'Error: ' + errorMessage;
messageContainer.appendChild(errorDiv);
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Handle code actions
document.addEventListener('click', (e) => {
if (e.target.classList.contains('copy-button')) {
vscode.postMessage({
@ -115,10 +394,113 @@ exports.scripts = /*javascript*/`
});
}
// Hide suggestions when clicking outside
const suggestionsDiv = document.getElementById('suggestions');
if (suggestionsDiv && !suggestionsDiv.contains(e.target) && e.target !== promptInput) {
if (suggestionsDiv && !suggestionsDiv.contains(e.target) && e.target !== document.getElementById('promptInput')) {
suggestionsDiv.style.display = 'none';
}
});
`;
const style = document.createElement('style');
style.textContent = \`
#contextContainer {
padding: 10px;
background: #252526;
border-bottom: 1px solid #444;
color: #d4d4d4;
}
.context-item {
margin: 5px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.remove-context {
margin-left: 10px;
padding: 2px 5px;
background: #dc3545;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
#suggestions {
position: absolute;
width: 300px;
max-height: 200px;
overflow-y: auto;
background: #1e1e1e;
border: 1px solid #444;
border-radius: 3px;
padding: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.5);
z-index: 10000;
color: #d4d4d4;
}
.suggestion {
padding: 5px;
cursor: pointer;
}
.suggestion:hover {
background: #333;
}
.history-button {
margin: 10px;
padding: 5px 10px;
background: #007acc;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.history-button:hover {
background: #005f9e;
}
.history-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
}
.history-content {
background: #1e1e1e;
padding: 20px;
border-radius: 5px;
max-width: 80%;
max-height: 80%;
overflow: auto;
color: #d4d4d4;
}
.history-item {
padding: 5px;
margin: 5px 0;
border-radius: 3px;
cursor: pointer;
background: #252526;
}
.history-item:hover {
background: #333;
}
.close-history {
margin-top: 10px;
padding: 5px 10px;
background: #dc3545;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.message.error {
background: #721c24;
color: #f8d7da;
padding: 10px;
margin: 5px 0;
border-radius: 3px;
}
\`;
document.head.appendChild(style);
`;

View File

@ -1,9 +1,17 @@
// styles.js
exports.styles = /*css*/ `
exports.styles =/*css*/ `
body {
padding: 16px;
height: 100vh;
margin: 0;
display: flex;
flex-direction: column;
}
.git-info {
padding: 8px;
background: var(--vscode-editor-inactiveSelectionBackground);
border-radius: var(--border-radius);
border-radius: 4px;
margin-bottom: 8px;
font-size: 0.9em;
}
@ -13,11 +21,66 @@ exports.styles = /*css*/ `
color: var(--vscode-descriptionForeground);
}
.chat-container {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.message-container {
flex: 1;
overflow-y: auto;
margin-bottom: 16px;
padding: 8px;
}
.message {
margin-bottom: 16px;
padding: 8px;
border-radius: 4px;
background: var(--vscode-editor-inactiveSelectionBackground);
}
.message.user {
background: var(--vscode-editor-selectionBackground);
}
.input-container {
display: flex;
gap: 8px;
padding: 8px;
background: var(--vscode-editor-background);
border-top: 1px solid var(--vscode-panel-border);
}
.prompt-input {
flex: 1;
padding: 8px;
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
}
.send-button {
padding: 8px 16px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
}
.send-button:hover {
background: var(--vscode-button-hoverBackground);
}
.suggestions {
position: absolute;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: var(--border-radius);
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
@ -33,27 +96,9 @@ exports.styles = /*css*/ `
background: var(--vscode-list-hoverBackground);
}
.chat-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px;
}
.message-container {
flex: 1;
overflow-y: auto;
margin-bottom: 16px;
}
.input-container {
display: flex;
gap: 8px;
}
.code-block-wrapper {
margin: 16px 0;
border-radius: var(--border-radius);
border-radius: 4px;
border: 1px solid var(--vscode-panel-border);
}
@ -96,4 +141,4 @@ exports.styles = /*css*/ `
padding: 16px;
overflow-x: auto;
}
`;
`;