Inital commit/ Base to the extension
parent
5c0dd9a556
commit
02ebd7b198
|
@ -1,132 +1,3 @@
|
||||||
# ---> Node
|
node_modules
|
||||||
# Logs
|
.vscode-test/
|
||||||
logs
|
*.vsix
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
|
||||||
web_modules/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional stylelint cache
|
|
||||||
.stylelintcache
|
|
||||||
|
|
||||||
# Microbundle cache
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
.parcel-cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
.cache/
|
|
||||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# vuepress v2.x temp and cache directory
|
|
||||||
.temp
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# Docusaurus cache and generated files
|
|
||||||
.docusaurus
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
.tern-port
|
|
||||||
|
|
||||||
# Stores VSCode versions used for testing VSCode extensions
|
|
||||||
.vscode-test
|
|
||||||
|
|
||||||
# yarn v2
|
|
||||||
.yarn/cache
|
|
||||||
.yarn/unplugged
|
|
||||||
.yarn/build-state.yml
|
|
||||||
.yarn/install-state.gz
|
|
||||||
.pnp.*
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { defineConfig } from '@vscode/test-cli';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
files: 'test/**/*.test.js',
|
||||||
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"ms-vscode.extension-test-runner"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// A launch configuration that launches the extension inside a new window
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Run Extension",
|
||||||
|
"type": "extensionHost",
|
||||||
|
"request": "launch",
|
||||||
|
"args": [
|
||||||
|
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
.vscode/**
|
||||||
|
.vscode-test/**
|
||||||
|
test/**
|
||||||
|
.gitignore
|
||||||
|
.yarnrc
|
||||||
|
vsc-extension-quickstart.md
|
||||||
|
**/jsconfig.json
|
||||||
|
**/*.map
|
||||||
|
**/eslint.config.mjs
|
||||||
|
**/.vscode-test.*
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to the "93m1n1gpt" extension will be documented in this file.
|
||||||
|
|
||||||
|
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Initial release
|
66
README.md
66
README.md
|
@ -1,3 +1,65 @@
|
||||||
# 93m1n1gpt
|
# 93m1n1gpt README
|
||||||
|
|
||||||
A local llm pair programming ai assistant.
|
This is the README for your extension "93m1n1gpt". After writing up a brief description, we recommend including the following sections.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file.
|
||||||
|
|
||||||
|
For example if there is an image subfolder under your extension project workspace:
|
||||||
|
|
||||||
|
\!\[feature X\]\(images/feature-x.png\)
|
||||||
|
|
||||||
|
> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
If you have any requirements or dependencies, add a section describing those and how to install and configure them.
|
||||||
|
|
||||||
|
## Extension Settings
|
||||||
|
|
||||||
|
Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
This extension contributes the following settings:
|
||||||
|
|
||||||
|
* `myExtension.enable`: Enable/disable this extension.
|
||||||
|
* `myExtension.thing`: Set to `blah` to do something.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
Calling out known issues can help limit users opening duplicate issues against your extension.
|
||||||
|
|
||||||
|
## Release Notes
|
||||||
|
|
||||||
|
Users appreciate release notes as you update your extension.
|
||||||
|
|
||||||
|
### 1.0.0
|
||||||
|
|
||||||
|
Initial release of ...
|
||||||
|
|
||||||
|
### 1.0.1
|
||||||
|
|
||||||
|
Fixed issue #.
|
||||||
|
|
||||||
|
### 1.1.0
|
||||||
|
|
||||||
|
Added features X, Y, and Z.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Working with Markdown
|
||||||
|
|
||||||
|
You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts:
|
||||||
|
|
||||||
|
* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux)
|
||||||
|
* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux)
|
||||||
|
* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets
|
||||||
|
|
||||||
|
## For more information
|
||||||
|
|
||||||
|
* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown)
|
||||||
|
* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/)
|
||||||
|
|
||||||
|
**Enjoy!**
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
export default [{
|
||||||
|
files: ["**/*.js"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.commonjs,
|
||||||
|
...globals.node,
|
||||||
|
...globals.mocha,
|
||||||
|
},
|
||||||
|
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
"no-const-assign": "warn",
|
||||||
|
"no-this-before-super": "warn",
|
||||||
|
"no-undef": "warn",
|
||||||
|
"no-unreachable": "warn",
|
||||||
|
"no-unused-vars": "warn",
|
||||||
|
"constructor-super": "warn",
|
||||||
|
"valid-typeof": "warn",
|
||||||
|
},
|
||||||
|
}];
|
|
@ -0,0 +1,50 @@
|
||||||
|
const vscode = require("vscode");
|
||||||
|
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');
|
||||||
|
|
||||||
|
async function activate(context) {
|
||||||
|
// Initialize secure settings manager first
|
||||||
|
const settingsManager = new SecureSettingsManager(context);
|
||||||
|
await settingsManager.initialize();
|
||||||
|
|
||||||
|
// Initialize RepoTracker
|
||||||
|
const repoTracker = new RepoTracker();
|
||||||
|
await repoTracker.initialize();
|
||||||
|
|
||||||
|
// Set up git watcher
|
||||||
|
const gitWatcher = await repoTracker.watchGitChanges();
|
||||||
|
context.subscriptions.push(gitWatcher);
|
||||||
|
|
||||||
|
// Initialize providers with secure settings manager and repoTracker
|
||||||
|
const provider = new AISidebarProvider(context.extensionUri, settingsManager, repoTracker);
|
||||||
|
|
||||||
|
// Register the sidebar provider
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.window.registerWebviewViewProvider('C4B3Rstudios.93m1n1gpt', provider)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register the LLM selector command
|
||||||
|
let llmSelectorCommand = vscode.commands.registerCommand(
|
||||||
|
'C4B3Rstudios.93m1n1gpt.showLLMSelector',
|
||||||
|
() => {
|
||||||
|
const llmSelector = new LLMSelectorPanel(context.extensionUri, settingsManager);
|
||||||
|
llmSelector.createOrShow(vscode.ViewColumn.One);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register the main command
|
||||||
|
let mainCommand = vscode.commands.registerCommand('C4B3Rstudios.93m1n1gpt', () => {
|
||||||
|
vscode.window.showInformationMessage('93m1n1gpt is now active!');
|
||||||
|
});
|
||||||
|
|
||||||
|
context.subscriptions.push(llmSelectorCommand, mainCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivate() { }
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
activate,
|
||||||
|
deactivate
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "Node16",
|
||||||
|
"target": "ES2022",
|
||||||
|
"checkJs": true, /* Typecheck .js files. */
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,83 @@
|
||||||
|
{
|
||||||
|
"name": "93m1n1gpt",
|
||||||
|
"displayName": "93m1n1gpt",
|
||||||
|
"description": "Agent flow BYOLLM pair programming assistint ",
|
||||||
|
"publisher": "C4B3Rstudios",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.97.0"
|
||||||
|
},
|
||||||
|
"categories": [
|
||||||
|
"Other"
|
||||||
|
],
|
||||||
|
"activationEvents": [],
|
||||||
|
"main": "./extension.js",
|
||||||
|
"contributes": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "C4B3Rstudios.93m1n1gpt",
|
||||||
|
"title": "Open 93m1n1GPT",
|
||||||
|
"category": "93m1n1GPT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "C4B3Rstudios.93m1n1gpt.showLLMSelector",
|
||||||
|
"title": "93m1n1GPT: Show LLM Selector"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"viewsContainers": {
|
||||||
|
"activitybar": [
|
||||||
|
{
|
||||||
|
"id": "93m1n1gpt-sidebar",
|
||||||
|
"title": "93m1n1GPT",
|
||||||
|
"icon": "resources/icon.svg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"93m1n1gpt-sidebar": [
|
||||||
|
{
|
||||||
|
"type": "webview",
|
||||||
|
"id": "C4B3Rstudios.93m1n1gpt",
|
||||||
|
"name": "93m1n1GPT Chat",
|
||||||
|
"icon": "resources/icon.svg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"title": "93m1n1GPT",
|
||||||
|
"properties": {
|
||||||
|
"93m1n1gpt.selectedModel": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "llama3.2:3b",
|
||||||
|
"description": "Selected LLM model to use"
|
||||||
|
},
|
||||||
|
"93m1n1gpt.apiUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "http://localhost:11434",
|
||||||
|
"description": "Ollama API URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint .",
|
||||||
|
"pretest": "npm run lint",
|
||||||
|
"test": "vscode-test"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/mocha": "^10.0.10",
|
||||||
|
"@types/node": "20.x",
|
||||||
|
"@types/vscode": "^1.97.0",
|
||||||
|
"@vscode/test-cli": "^0.0.10",
|
||||||
|
"@vscode/test-electron": "^2.4.1",
|
||||||
|
"eslint": "^9.19.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@langchain/community": "^0.3.29",
|
||||||
|
"@langchain/core": "^0.3.39",
|
||||||
|
"@langchain/ollama": "^0.2.0",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"marked": "^15.0.7"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 12V22H4V2H20V12ZM18 14H6V20H18V14ZM18 4H6V12H18V4Z" fill="#424242"/>
|
||||||
|
<path d="M8 6H16V8H8V6Z" fill="#424242"/>
|
||||||
|
<path d="M8 16H16V18H8V16Z" fill="#424242"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 284 B |
|
@ -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,348 @@
|
||||||
|
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 'selectLLM':
|
||||||
|
await vscode.workspace.getConfiguration('93m1n1gpt').update(
|
||||||
|
'selectedModel',
|
||||||
|
message.model,
|
||||||
|
vscode.ConfigurationTarget.Global
|
||||||
|
);
|
||||||
|
vscode.window.showInformationMessage(`Selected LLM: ${message.model}`);
|
||||||
|
this.panel.dispose();
|
||||||
|
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';
|
||||||
|
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="llmSelector">Available Models</label>
|
||||||
|
<select id="llmSelector">
|
||||||
|
<option value="">Loading available models...</option>
|
||||||
|
</select>
|
||||||
|
<div class="model-count" id="modelCount"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status" class="status" style="display: none;"></div>
|
||||||
|
|
||||||
|
<div class="button-container">
|
||||||
|
<button id="selectButton" disabled>Select 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 llmSelector = document.getElementById('llmSelector');
|
||||||
|
const selectButton = document.getElementById('selectButton');
|
||||||
|
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 modelCountDiv = document.getElementById('modelCount');
|
||||||
|
|
||||||
|
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 populateLLMSelector() {
|
||||||
|
showStatus('Fetching available models...');
|
||||||
|
const llms = await fetchAvailableLLMs();
|
||||||
|
|
||||||
|
llmSelector.innerHTML = '';
|
||||||
|
if (llms.length === 0) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = '';
|
||||||
|
option.textContent = 'No models found';
|
||||||
|
llmSelector.appendChild(option);
|
||||||
|
selectButton.disabled = true;
|
||||||
|
modelCountDiv.textContent = 'No models available';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOption = document.createElement('option');
|
||||||
|
defaultOption.value = '';
|
||||||
|
defaultOption.textContent = 'Select a model';
|
||||||
|
llmSelector.appendChild(defaultOption);
|
||||||
|
|
||||||
|
llms.forEach(llm => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = llm.name;
|
||||||
|
option.textContent = llm.name;
|
||||||
|
llmSelector.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelCountDiv.textContent = \`\${llms.length} model\${llms.length === 1 ? '' : 's'} available\`;
|
||||||
|
selectButton.disabled = true;
|
||||||
|
statusDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
llmSelector.addEventListener('change', () => {
|
||||||
|
selectButton.disabled = !llmSelector.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
selectButton.addEventListener('click', () => {
|
||||||
|
const selectedLLM = llmSelector.value;
|
||||||
|
if (selectedLLM) {
|
||||||
|
vscode.postMessage({
|
||||||
|
command: 'selectLLM',
|
||||||
|
model: selectedLLM
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
saveApiUrlButton.addEventListener('click', async () => {
|
||||||
|
showStatus('Updating API URL...');
|
||||||
|
await vscode.postMessage({
|
||||||
|
command: 'updateApiUrl',
|
||||||
|
url: apiUrlInput.value
|
||||||
|
});
|
||||||
|
populateLLMSelector();
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshButton.addEventListener('click', () => {
|
||||||
|
populateLLMSelector();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the selector
|
||||||
|
populateLLMSelector();
|
||||||
|
</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 };
|
|
@ -0,0 +1,232 @@
|
||||||
|
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);
|
||||||
|
|
||||||
|
class RepoTracker {
|
||||||
|
constructor() {
|
||||||
|
this.workspaceRoot = "";
|
||||||
|
this.fileTree = [];
|
||||||
|
this.gitInfo = null;
|
||||||
|
this.ignoredPaths = [
|
||||||
|
'node_modules',
|
||||||
|
'.git',
|
||||||
|
'dist',
|
||||||
|
'build',
|
||||||
|
'.next',
|
||||||
|
'.cache'
|
||||||
|
];
|
||||||
|
this._initializeCompletionProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders) return;
|
||||||
|
|
||||||
|
this.workspaceRoot = workspaceFolders[0].uri.fsPath;
|
||||||
|
await this.buildFileTree();
|
||||||
|
await this.initializeGitTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initializeGitTracking() {
|
||||||
|
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();
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Not a git repository or git not installed: ', error);
|
||||||
|
this.gitInfo = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanDirectory(dirPath) {
|
||||||
|
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileContent(filePath) {
|
||||||
|
const fullPath = path.join(this.workspaceRoot, filePath);
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(fullPath, 'utf-8');
|
||||||
|
return content;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reading file ${filePath}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileTreeSummary() {
|
||||||
|
return JSON.stringify(this.fileTree, null, 2);
|
||||||
|
}
|
||||||
|
getGitStatus() {
|
||||||
|
return this.gitInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async watchGitChanges() {
|
||||||
|
// Set up a file system watcher for git changes
|
||||||
|
const gitWatcher = vscode.workspace.createFileSystemWatcher(
|
||||||
|
new vscode.RelativePattern(this.workspaceRoot, '.git/**')
|
||||||
|
);
|
||||||
|
|
||||||
|
gitWatcher.onDidChange(() => this.updateGitStatus());
|
||||||
|
gitWatcher.onDidCreate(() => this.updateGitStatus());
|
||||||
|
gitWatcher.onDidDelete(() => this.updateGitStatus());
|
||||||
|
|
||||||
|
return gitWatcher;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { RepoTracker };
|
|
@ -0,0 +1,62 @@
|
||||||
|
// 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 `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>${styles}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="gitStatus"></div>
|
||||||
|
<div id="chatContainer" 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,231 @@
|
||||||
|
const vscode = require("vscode");
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const util = require('util');
|
||||||
|
const execPromise = util.promisify(exec);
|
||||||
|
|
||||||
|
class RepoTracker {
|
||||||
|
constructor() {
|
||||||
|
this.workspaceRoot = "";
|
||||||
|
this.fileTree = [];
|
||||||
|
this.gitInfo = null;
|
||||||
|
this.ignoredPaths = [
|
||||||
|
'node_modules',
|
||||||
|
'.git',
|
||||||
|
'dist',
|
||||||
|
'build',
|
||||||
|
'.next',
|
||||||
|
'.cache'
|
||||||
|
];
|
||||||
|
this._initializeCompletionProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||||
|
if (!workspaceFolders) return;
|
||||||
|
|
||||||
|
this.workspaceRoot = workspaceFolders[0].uri.fsPath;
|
||||||
|
await this.buildFileTree();
|
||||||
|
await this.initializeGitTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initializeGitTracking() {
|
||||||
|
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();
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Not a git repository or git not installed');
|
||||||
|
this.gitInfo = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... (keep existing methods: buildFileTree, scanDirectory, getFileContent, getFileTreeSummary)
|
||||||
|
async buildFileTree() {
|
||||||
|
this.fileTree = await this.scanDirectory(this.workspaceRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanDirectory(dirPath) {
|
||||||
|
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileContent(filePath) {
|
||||||
|
const fullPath = path.join(this.workspaceRoot, filePath);
|
||||||
|
try {
|
||||||
|
const content = await fs.promises.readFile(fullPath, 'utf-8');
|
||||||
|
return content;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reading file ${filePath}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileTreeSummary() {
|
||||||
|
return JSON.stringify(this.fileTree, null, 2);
|
||||||
|
}
|
||||||
|
getGitStatus() {
|
||||||
|
return this.gitInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async watchGitChanges() {
|
||||||
|
// Set up a file system watcher for git changes
|
||||||
|
const gitWatcher = vscode.workspace.createFileSystemWatcher(
|
||||||
|
new vscode.RelativePattern(this.workspaceRoot, '.git/**')
|
||||||
|
);
|
||||||
|
|
||||||
|
gitWatcher.onDidChange(() => this.updateGitStatus());
|
||||||
|
gitWatcher.onDidCreate(() => this.updateGitStatus());
|
||||||
|
gitWatcher.onDidDelete(() => this.updateGitStatus());
|
||||||
|
|
||||||
|
return gitWatcher;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { RepoTracker };
|
|
@ -0,0 +1,124 @@
|
||||||
|
// scripts.js
|
||||||
|
exports.scripts = /*javascript*/`
|
||||||
|
let fileTree = [];
|
||||||
|
let gitStatus = null;
|
||||||
|
const vscode = acquireVsCodeApi();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle git status updates
|
||||||
|
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':
|
||||||
|
updateChatDisplay(message.html);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 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 updateChatDisplay(html) {
|
||||||
|
const chatContainer = document.getElementById('chatContainer');
|
||||||
|
if (chatContainer) {
|
||||||
|
chatContainer.innerHTML = html;
|
||||||
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 !== promptInput) {
|
||||||
|
suggestionsDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`;
|
|
@ -0,0 +1,99 @@
|
||||||
|
// styles.js
|
||||||
|
exports.styles = /*css*/ `
|
||||||
|
.git-info {
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info span {
|
||||||
|
margin-right: 16px;
|
||||||
|
color: var(--vscode-descriptionForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border: 1px solid var(--vscode-panel-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion:hover {
|
||||||
|
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: 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;
|
|
@ -0,0 +1,15 @@
|
||||||
|
import * as assert from 'assert';
|
||||||
|
|
||||||
|
// You can import and use all API from the 'vscode' module
|
||||||
|
// as well as import your extension to test it
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
// const myExtension = require('../extension');
|
||||||
|
|
||||||
|
suite('Extension Test Suite', () => {
|
||||||
|
vscode.window.showInformationMessage('Start all tests.');
|
||||||
|
|
||||||
|
test('Sample test', () => {
|
||||||
|
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
|
||||||
|
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Welcome to your VS Code Extension
|
||||||
|
|
||||||
|
## What's in the folder
|
||||||
|
|
||||||
|
* This folder contains all of the files necessary for your extension.
|
||||||
|
* `package.json` - this is the manifest file in which you declare your extension and command.
|
||||||
|
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
|
||||||
|
* `extension.js` - this is the main file where you will provide the implementation of your command.
|
||||||
|
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
|
||||||
|
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
|
||||||
|
|
||||||
|
## Get up and running straight away
|
||||||
|
|
||||||
|
* Press `F5` to open a new window with your extension loaded.
|
||||||
|
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
|
||||||
|
* Set breakpoints in your code inside `extension.js` to debug your extension.
|
||||||
|
* Find output from your extension in the debug console.
|
||||||
|
|
||||||
|
## Make changes
|
||||||
|
|
||||||
|
* You can relaunch the extension from the debug toolbar after changing code in `extension.js`.
|
||||||
|
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
|
||||||
|
|
||||||
|
## Explore the API
|
||||||
|
|
||||||
|
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
|
||||||
|
* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
|
||||||
|
* See the output of the test result in the Test Results view.
|
||||||
|
* Make changes to `test/extension.test.js` or create new test files inside the `test` folder.
|
||||||
|
* The provided test runner will only consider files matching the name pattern `**.test.js`.
|
||||||
|
* You can create folders inside the `test` folder to structure your tests any way you want.
|
||||||
|
|
||||||
|
## Go further
|
||||||
|
|
||||||
|
* [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns.
|
||||||
|
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
|
||||||
|
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
|
||||||
|
* Integrate to the [report issue](https://code.visualstudio.com/api/get-started/wrapping-up#issue-reporting) flow to get issue and feature requests reported by users.
|
Loading…
Reference in New Issue