Added code completion, @file context
parent
02ebd7b198
commit
1bd565e16b
|
@ -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!');
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 };
|
|
@ -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>
|
|
@ -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 };
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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);
|
||||
}
|
|
@ -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 };
|
|
@ -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';
|
||||
}
|
||||
});
|
||||
`;
|
|
@ -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;
|
||||
}
|
||||
`;
|
|
@ -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;
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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}
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
`;
|
|
@ -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;
|
||||
}
|
||||
`;
|
||||
`;
|
Loading…
Reference in New Issue