Merge pull request #4 from covespace/replace_gpt_with_devchat

Replace gpt with devchat
This commit is contained in:
boob.yang 2023-04-28 13:29:00 +08:00 committed by GitHub
commit 523b0fb35c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 5797 additions and 391 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ node_modules
.vscode-test/
*.vsix
.env
.chatconfig.json

View File

@ -1,71 +1,46 @@
# devchat README
# DevChat
This is the README for your extension "devchat". After writing up a brief description, we recommend including the following sections.
DevChat is a programming assistant that helps developers, testers, and operations staff automate many tasks.
## 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.
- Generate code driven by questions
- Create smarter commit messages
## Requirements
If you have any requirements or dependencies, add a section describing those and how to install and configure them.
- Python package: `pip install devchat`
- System package: `apt install dtm`
- Set your `OPENAI_API_KEY` environment variable
## Extension Settings
## Installation
Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
Search for "DevChat" in the Visual Studio Code marketplace and click the "Install" button.
For example:
## Usage
This extension contributes the following settings:
Enter "DevChat" in the command palette to open the interactive page.
* `myExtension.enable`: Enable/disable this extension.
* `myExtension.thing`: Set to `blah` to do something.
## Settings
*Coming soon*
## Known Issues
Calling out known issues can help limit users opening duplicate issues against your extension.
*Coming soon*
## Release Notes
Users appreciate release notes as you update your extension.
*Coming soon*
### 1.0.0
## Contributing
Initial release of ...
*Coming soon*
### 1.0.1
## License
Fixed issue #.
*Specify the license here*
### 1.1.0
## Contact Information
Added features X, Y, and Z.
---
## Following extension guidelines
Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension.
* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
## 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!**
*Provide your contact information, such as an email address or a link to a GitHub repository*

5
jest.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["<rootDir>/src/__test__/**/*.test.ts"],
};

View File

@ -47,6 +47,22 @@ pre {
overflow: auto;
}
.context-menu {
position: absolute;
display: none;
background-color: white;
border: 1px solid #ccc;
padding: 8px;
z-index: 10;
}
.context-menu-item {
cursor: pointer;
margin-bottom: 4px;
}
.context-menu-item:last-child {
margin-bottom: 0;
}
.copy-button {
background-color: #007acc;
color: white;

View File

@ -26,6 +26,11 @@
</div>
</div>
<div class="context-menu" id="context-menu">
<div class="context-menu-item" id="menu-item-1">Insert Code</div>
<div class="context-menu-item" id="menu-item-2">Copy</div>
</div>
<!-- Load the chatPanel.js file -->
<script src="<vscode-resource:/chatUI.js>"></script>
<script src="<vscode-resource:/resizeInput.js>"></script>

View File

@ -1,11 +1,71 @@
// chatUI.js
const vscode_api = acquireVsCodeApi();
const md = new markdownit();
function addMessageToUI(role, content) {
function getLastBotMessageItem(messagesContainer) {
const lastMessage = messagesContainer.lastElementChild;
if (lastMessage && lastMessage.classList.contains('message-item')) {
const lastMessageIcon = lastMessage.querySelector('i');
if (lastMessageIcon && lastMessageIcon.classList.contains('fa-robot')) {
return lastMessage;
}
}
return null;
}
function initButtonForCodeBlock(codeBlocks) {
codeBlocks.forEach(block => {
block.classList.add('code-block');
});
initClipboard(codeBlocks, (patchContent) => {
postVSCodeMessage({
command: 'block_apply',
content: patchContent,
});
}, (codeContent) => {
postVSCodeMessage({
command: 'code_apply',
content: codeContent,
});
}, (codeContent) => {
postVSCodeMessage({
command: 'code_file_apply',
content: codeContent,
});
});
}
function addMessageToUI(role, content, partial = false) {
// Create a MarkdownIt instance for rendering markdown content
const messagesContainer = document.getElementById('messages-container');
// Create a container for the message item
// Render the markdown content inside the message content container
let renderedContent = md.render(content);
if (role == "user") {
renderedContent = md.render("\`\`\`\n" + content);
}
let lastBotMessage = getLastBotMessageItem(messagesContainer);
let lastMessageContent;
if (role == 'bot' && lastBotMessage != null) {
lastMessageContent = lastBotMessage.querySelector('.message-content');
lastMessageContent.innerHTML = renderedContent;
if (!partial) {
// Find any code blocks in the rendered content and add a class to style them
const codeBlocks = lastMessageContent.querySelectorAll('pre > code');
// Initialize the Apply Patch functionality
initButtonForCodeBlock(codeBlocks);
}
return;
}
const messageItem = document.createElement('div');
messageItem.classList.add('message-item');
@ -17,18 +77,16 @@ function addMessageToUI(role, content) {
// Create a container for the message content
const messageContent = document.createElement('div');
messageContent.classList.add('message-content');
// Render the markdown content inside the message content container
const renderedContent = md.render(content);
messageContent.innerHTML = renderedContent;
// Find any code blocks in the rendered content and add a class to style them
const codeBlocks = messageContent.querySelectorAll('pre > code');
codeBlocks.forEach(block => {
block.classList.add('code-block');
});
initClipboard(codeBlocks);
// Initialize the Apply Patch functionality
if (role != "user") {
initButtonForCodeBlock(codeBlocks);
}
messageItem.appendChild(senderIcon);
messageItem.appendChild(messageContent);
@ -121,4 +179,16 @@ function processMessage(message) {
function processMessageUI(message) {
addMessageToUI('user', message);
processMessage(message);
}
// Function to request history messages from the extension
function requestHistoryMessages() {
// Send a message to the extension with the 'historyMessages' command
vscode_api.postMessage({
command: 'historyMessages',
});
}
function postVSCodeMessage(message) {
vscode_api.postMessage(message);
}

View File

@ -1,5 +1,13 @@
function initClipboard(codeBlocks) {
// clipboard.js
function initClipboard(codeBlocks, onApplyButtonClick, onApplyCodeButtonClick, onApplyCodeFileButtonClick) {
codeBlocks.forEach(block => {
const contentSpan = document.createElement('span');
contentSpan.innerHTML = block.innerHTML;
block.innerHTML = '';
block.appendChild(contentSpan);
const copyButton = document.createElement('button');
copyButton.classList.add('copy-button');
copyButton.innerText = 'Copy';
@ -7,7 +15,7 @@ function initClipboard(codeBlocks) {
copyButton.addEventListener('click', () => {
// Copy the message text to the clipboard
navigator.clipboard.writeText(block.textContent);
navigator.clipboard.writeText(contentSpan.textContent);
// Change the button text temporarily to show that the text has been copied
copyButton.textContent = 'Copied!';
@ -20,5 +28,35 @@ function initClipboard(codeBlocks) {
copyButton.appendChild(copyIcon);
}, 1000);
});
// Add 'Apply' button
const applyButton = document.createElement('button');
applyButton.classList.add('apply-button');
applyButton.innerText = 'Apply Patch';
block.appendChild(applyButton);
applyButton.addEventListener('click', () => {
onApplyButtonClick(contentSpan.textContent);
});
// Add 'Apply' button
const applyCodeButton = document.createElement('button');
applyCodeButton.classList.add('apply-button');
applyCodeButton.innerText = 'Insert Code';
block.appendChild(applyCodeButton);
applyCodeButton.addEventListener('click', () => {
onApplyCodeButtonClick(contentSpan.textContent);
});
// Add 'Apply' button
const applyCodeFileButton = document.createElement('button');
applyCodeFileButton.classList.add('apply-button');
applyCodeFileButton.innerText = 'Relace File';
block.appendChild(applyCodeFileButton);
applyCodeFileButton.addEventListener('click', () => {
onApplyCodeFileButtonClick(contentSpan.textContent);
});
});
}

View File

@ -1,46 +1,123 @@
// main.js
(function () {
// Get DOM elements for user interaction and message display
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
// Get DOM elements for user interaction and message display
const messagesContainer = document.getElementById('messages-container');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const contextMenu = document.getElementById('context-menu');
const menuItem1 = document.getElementById('menu-item-1');
const menuItem2 = document.getElementById('menu-item-2');
let selectedText = '';
// Initialize input resizing
initInputResizing();
function hideContextMenu() {
contextMenu.style.display = 'none';
}
function getSelectedText() {
const selection = window.getSelection();
return selection.toString();
}
// Initialize input resizing
initInputResizing();
messagesContainer.addEventListener('contextmenu', (event) => {
event.preventDefault();
selectedText = getSelectedText();
contextMenu.style.display = 'block';
contextMenu.style.left = event.pageX + 'px';
contextMenu.style.top = event.pageY + 'px';
});
// Event listener for receiving messages from the extension
window.addEventListener('message', (event) => {
const message = event.data;
switch (message.command) {
case 'receiveMessage':
// Add the received message to the chat UI as a bot message
addMessageToUI('bot', message.text);
break;
}
document.addEventListener('click', hideContextMenu);
menuItem1.addEventListener('click', () => {
postVSCodeMessage({
command: 'code_apply',
content: selectedText,
});
hideContextMenu();
});
// Event listener for the send button
sendButton.addEventListener('click', () => {
const message = messageInput.value;
if (message) {
// Add the user's message to the chat UI
addMessageToUI('user', message);
// Clear the input field
messageInput.value = '';
// Process and send the message to the extension
processMessage(message);
menuItem2.addEventListener('click', () => {
navigator.clipboard.writeText(selectedText);
hideContextMenu();
});
// Event listener for receiving messages from the extension
window.addEventListener('message', (event) => {
const message = event.data;
switch (message.command) {
case 'receiveMessage':
// Add the received message to the chat UI as a bot message
addMessageToUI('bot', message.text);
break;
case 'receiveMessagePartial':
// Add the received message to the chat UI as a bot message
addMessageToUI('bot', message.text, true);
break;
case 'loadHistoryMessages':
loadHistoryMessages(message.entries);
break;
case 'file_select':
addFileToMessageInput(message.filePath);
break;
case 'code_select':
addCodeToMessageInput(message.codeBlock);
break
case 'ask_ai':
message_text = message.codeBlock + "\n" + message.question;
processMessageUI(message_text)
break;
}
});
// Event listener for the send button
sendButton.addEventListener('click', () => {
const message = messageInput.value;
if (message) {
// Add the user's message to the chat UI
addMessageToUI('user', message);
// Clear the input field
messageInput.value = '';
// Process and send the message to the extension
processMessage(message);
}
});
// Event listener for the Enter key in the message input field
messageInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const message = messageInput.value.trim();
if (message !== '') {
sendButton.click();
}
});
// Event listener for the Enter key in the message input field
messageInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const message = messageInput.value.trim();
if (message !== '') {
sendButton.click();
}
}
});
})();
}
});
function addFileToMessageInput(filePath) {
const formattedPath = `[context|${filePath}] `;
messageInput.value = formattedPath + messageInput.value;
messageInput.focus();
}
function addCodeToMessageInput(codeBlock) {
messageInput.value += "\n" + codeBlock + "\n";
messageInput.focus();
}
// Request history messages when the web view is created and shown
requestHistoryMessages();
})();
// Function to load history messages from the extension
function loadHistoryMessages(entries) {
entries.forEach((entry) => {
addMessageToUI('user', entry.message);
addMessageToUI('bot', entry.response);
});
}

4961
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,9 +23,44 @@
"commands": [
{
"command": "devchat.openChatPanel",
"title": "Open Chat Panel"
"title": "DevChat"
},
{
"command": "devchat.addConext",
"title": "add to DevChat"
},
{
"command": "devchat.askForCode",
"title": "add to DevChat"
},
{
"command": "devchat.askForFile",
"title": "add to DevChat"
}
]
],
"menus": {
"explorer/context": [
{
"when": "resourceLangId != 'git'",
"command": "devchat.addConext",
"group": "navigation"
}
],
"editor/context": [
{
"command": "devchat.askForCode",
"when": "editorTextFocus && editorHasSelection",
"group": "navigation",
"title": "add to DevChat"
},
{
"command": "devchat.askForFile",
"when": "editorTextFocus && !editorHasSelection",
"group": "navigation",
"title": "add to DevChat"
}
]
}
},
"scripts": {
"vscode:prepublish": "npm run package",
@ -40,15 +75,19 @@
},
"devDependencies": {
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.1",
"@types/mocha": "^10.0.1",
"@types/node": "16.x",
"@types/shell-escape": "^0.2.1",
"@types/vscode": "^1.77.0",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"@vscode/test-electron": "^2.3.0",
"eslint": "^8.36.0",
"glob": "^8.1.0",
"jest": "^29.5.0",
"mocha": "^10.2.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
"webpack": "^5.76.3",
@ -60,6 +99,8 @@
"node-fetch": "^3.3.1",
"nonce": "^1.0.4",
"openai": "^3.2.1",
"quote": "^0.4.0",
"shell-escape": "^0.2.0",
"uuid": "^9.0.0"
}
}

46
src/applyCode.ts Normal file
View File

@ -0,0 +1,46 @@
const vscode = require('vscode');
export async function applyCodeFile(text: string) {
if (vscode.window.visibleTextEditors.length > 1) {
vscode.window.showErrorMessage(`There are more then one visible text editors. Please close all but one and try again.`);
return;
}
const editor = vscode.window.visibleTextEditors[0];
if (!editor) {
return;
}
const document = editor.document;
const fullRange = new vscode.Range(
document.positionAt(0),
document.positionAt(document.getText().length)
);
await editor.edit((editBuilder: string) => {
editBuilder.replace(fullRange, text);
});
}
async function applyCode(text: string) {
if (vscode.window.visibleTextEditors.length > 1) {
vscode.window.showErrorMessage(`There are more then one visible text editors. Please close all but one and try again.`);
return;
}
const editor = vscode.window.visibleTextEditors[0];
if (!editor) {
return;
}
const selection = editor.selection;
const start = selection.start;
const end = selection.end;
await editor.edit((editBuilder: string) => {
editBuilder.replace(new vscode.Range(start, end), text);
});
}
export default applyCode;

View File

@ -1,106 +1,100 @@
// chatPanel.ts
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import { v4 as uuidv4 } from 'uuid';
import { chatWithGPT } from './openaiClient';
import DevChat from './devchat';
import handleMessage from './messageHandler';
export default class ChatPanel {
public static currentPanel: ChatPanel | undefined;
private static _instance: ChatPanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private _session_id: string;
private _sessionName: string;
private _messageHistory: Array<{ role: string; content: string }>;
private _disposables: vscode.Disposable[] = [];
private static _sessions: { [sessionName: string]: ChatPanel } = {};
public static createOrShow(extensionUri: vscode.Uri, sessionName: string) {
const session = ChatPanel._sessions[sessionName];
if (session) {
// If a session with the given name exists, reveal the existing panel
session._panel.reveal();
return
// Create or reveal the chat panel
public static createOrShow(extensionUri: vscode.Uri) {
if (ChatPanel._instance) {
ChatPanel._instance._panel.reveal();
} else {
const panel = ChatPanel.createWebviewPanel(extensionUri);
ChatPanel._instance = new ChatPanel(panel, extensionUri);
}
}
public static currentPanel(): ChatPanel | undefined {
return ChatPanel._instance;
}
// Create a new webview panel
private static createWebviewPanel(extensionUri: vscode.Uri): vscode.WebviewPanel {
const column = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
// Create a new webview panel
const panel = vscode.window.createWebviewPanel(
return vscode.window.createWebviewPanel(
'chatPanel',
sessionName,
'Chat',
column || vscode.ViewColumn.One,
{
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')]
localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')],
retainContextWhenHidden: true
}
);
// Set the webview's initial HTML content
const chatPanel = new ChatPanel(panel, extensionUri, uuidv4(), sessionName);
ChatPanel._sessions[sessionName] = chatPanel;
}
public static sessions() {
return ChatPanel._sessions;
}
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, session_id: string, sessionName: string) {
// ... initialize the chat panel ...
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
this._panel = panel;
this._sessionName = sessionName;
this._session_id = session_id;
this._messageHistory = [];
// Set the webview options
this.setWebviewOptions(extensionUri);
this.setWebviewContent(extensionUri);
this.registerEventListeners();
}
// Set webview options
private setWebviewOptions(extensionUri: vscode.Uri) {
this._panel.webview.options = {
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')]
localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')],
};
}
// Set the webview content
// Set webview content
private setWebviewContent(extensionUri: vscode.Uri) {
this._panel.webview.html = this._getHtmlContent(extensionUri);
}
// Handle webview events and dispose of the panel when closed
public panel() : vscode.WebviewPanel {
return this._panel;
}
// Register event listeners for the panel and webview
private registerEventListeners() {
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
this._panel.webview.onDidReceiveMessage(
async (message) => {
switch (message.command) {
case 'sendMessage':
const [status, response] = await chatWithGPT(message.text, this._session_id, this._messageHistory);
if (status == 0) {
this._messageHistory.push({ role: 'user', content: message.text });
this._messageHistory.push({ role: 'assistant', content: response });
}
this._panel.webview.postMessage({ command: 'receiveMessage', text: response });
return;
}
handleMessage(message, this._panel);
},
null,
this._disposables
);
}
// Get the HTML content for the panel
private _getHtmlContent(extensionUri: vscode.Uri): string {
const htmlPath = vscode.Uri.joinPath(extensionUri, 'media', 'chatPanel.html');
const htmlContent = fs.readFileSync(htmlPath.fsPath, 'utf8');
// Replace the resource placeholder with the correct resource URI
return htmlContent.replace(/<vscode-resource:(\/.+?)>/g, (_, resourcePath) => {
const resourceUri = vscode.Uri.joinPath(extensionUri, 'media', resourcePath);
return this._panel.webview.asWebviewUri(resourceUri).toString();
});
}
// Dispose the panel and clean up resources
public dispose() {
// ... dispose the panel and clean up resources ...
// Remove the ChatPanel instance from the _sessions object
delete ChatPanel._sessions[this._sessionName];
// Dispose of the WebviewPanel and other resources
ChatPanel._instance = undefined;
this._panel.dispose();
while (this._disposables.length) {
const disposable = this._disposables.pop();
@ -109,6 +103,4 @@ export default class ChatPanel {
}
}
}
// ... other helper methods ...
}
}

204
src/devchat.ts Normal file
View File

@ -0,0 +1,204 @@
// devchat.ts
import { spawn } from "child_process";
import { promisify } from "util";
import * as vscode from 'vscode';
import * as dotenv from 'dotenv';
import * as path from 'path';
const spawnAsync = async (command: string, args: string[], options: any, onData: (data: string) => void): Promise<{code: number, stdout: string; stderr: string }> => {
return new Promise((resolve, reject) => {
const child = spawn(command, args, options);
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
const dataStr = data.toString();
onData(dataStr);
stdout += dataStr;
});
child.stderr.on('data', (data) => {
stderr += data;
});
child.on('close', (code) => {
if (code === 0) {
resolve({code, stdout, stderr });
} else {
reject({code, stdout, stderr });
}
});
});
};
const envPath = path.join(__dirname, '..', '.env');
dotenv.config({ path: envPath });
export interface ChatOptions {
parent?: string;
reference?: string[];
header?: string[];
context?: string[];
}
export interface LogOptions {
skip?: number;
maxCount?: number;
}
export interface LogEntry {
"prompt-hash": string;
user: string;
date: string;
message: string;
response: string;
}
export interface ChatResponse {
"prompt-hash": string;
user: string;
date: string;
response: string;
isError: boolean;
}
class DevChat {
async chat(content: string, options: ChatOptions = {}, onData: (data: string) => void): Promise<ChatResponse> {
let args = ["prompt"];
if (options.parent) {
args.push("-p", options.parent);
}
if (options.reference) {
args.push("-r", options.reference.join(","));
}
if (options.header) {
args.push("--header", options.header.join(","));
}
if (options.context) {
args.push("--context", options.context.join(","));
}
args.push(content)
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
const openaiApiKey = process.env.OPENAI_API_KEY;
try {
const {code, stdout, stderr } = await spawnAsync('devchat', args, {
maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB
cwd: workspaceDir,
env: {
...process.env,
OPENAI_API_KEY: openaiApiKey,
},
}, onData);
if (stderr) {
const errorMessage = stderr.trim().match(/Error(.+)/)?.[1];
return {
"prompt-hash": "",
user: "",
date: "",
response: errorMessage ? `Error: ${errorMessage}` : "Unknown error",
isError: true,
};
}
const responseLines = stdout.trim().split("\n");
console.log(responseLines)
if (responseLines.length === 0) {
return {
"prompt-hash": "",
user: "",
date: "",
response: "",
isError: true,
};
}
let promptHashLine = "";
for (let i = responseLines.length - 1; i >= 0; i--) {
if (responseLines[i].startsWith("prompt")) {
promptHashLine = responseLines[i];
responseLines.splice(i, 1);
break;
}
}
if (!promptHashLine) {
return {
"prompt-hash": "",
user: "",
date: "",
response: responseLines.join("\n"),
isError: true,
};
}
const promptHash = promptHashLine.split(" ")[1];
const userLine = responseLines.shift()!;
const user = (userLine.match(/User: (.+)/)?.[1]) ?? "";
const dateLine = responseLines.shift()!;
const date = (dateLine.match(/Date: (.+)/)?.[1]) ?? "";
const response = responseLines.join("\n");
return {
"prompt-hash": promptHash,
user,
date,
response,
isError: false,
};
} catch (error: any) {
return {
"prompt-hash": "",
user: "",
date: "",
response: `Error: ${error.stderr}\nExit code: ${error.code}`,
isError: true,
};
}
}
async log(options: LogOptions = {}): Promise<LogEntry[]> {
let args = ["log"];
if (options.skip) {
args.push('--skip', `${options.skip}`);
}
if (options.maxCount) {
args.push('--max-count', `${options.maxCount}`);
}
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
const openaiApiKey = process.env.OPENAI_API_KEY;
try {
const {code, stdout, stderr } = await spawnAsync('devchat', args, {
maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB
cwd: workspaceDir,
env: {
...process.env,
OPENAI_API_KEY: openaiApiKey,
},
}, (partialResponse: string) => {});
if (stderr) {
console.error(stderr);
return [];
}
return JSON.parse(stdout.trim());
} catch (error) {
console.error(error)
return [];
}
}
}
export default DevChat;

41
src/dtm.ts Normal file
View File

@ -0,0 +1,41 @@
// dtm.ts
import { exec } from "child_process";
import { promisify } from "util";
import * as vscode from 'vscode';
const execAsync = promisify(exec);
interface DtmResponse {
status: number;
message: string;
log: string;
}
class DtmWrapper {
private workspaceDir: string;
constructor() {
this.workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath || '.';
}
async scaffold(directoryTree: string): Promise<DtmResponse> {
const { stdout } = await execAsync(`dtm scaffold "${directoryTree}" -o json`, {
cwd: this.workspaceDir,
});
return JSON.parse(stdout.trim());
}
async patch(patchFilePath: string): Promise<DtmResponse> {
try {
const { stdout } = await execAsync(`dtm patch ${patchFilePath} -o json`, {
cwd: this.workspaceDir,
});
return JSON.parse(stdout.trim());
} catch (e) {
return JSON.parse((e as Error & { stdout: string }).stdout.trim());
}
}
}
export default DtmWrapper;

View File

@ -1,39 +1,69 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
const vscode = require('vscode');
const ChatPanel = require('./chatPanel').default;
const sendFileSelectMessage = require('./messageHandler').sendFileSelectMessage;
const sendCodeSelectMessage = require('./messageHandler').sendCodeSelectMessage;
const askAI = require('./messageHandler').askAI;
function activate(context: { extensionUri: any; subscriptions: any[]; }) {
let disposable = vscode.commands.registerCommand('devchat.openChatPanel', async () => {
const sessionNames = Object.keys(ChatPanel.sessions());
let disposable = vscode.commands.registerCommand('devchat.openChatPanel', () => {
if (vscode.workspace.workspaceFolders) {
ChatPanel.createOrShow(context.extensionUri);
} else {
vscode.window.showErrorMessage('Please open a directory before using the chat panel.');
}
});
const createNewSessionOption = 'Create new session';
const options = [...sessionNames, createNewSessionOption];
const disposable_add_context = vscode.commands.registerCommand('devchat.addConext', (uri: { path: any; }) => {
if (!ChatPanel.currentPanel()) {
if (vscode.workspace.workspaceFolders) {
ChatPanel.createOrShow(context.extensionUri);
} else {
vscode.window.showErrorMessage('Please open a directory before using the chat panel.');
return
}
}
const selectedOption = await vscode.window.showQuickPick(options, {
placeHolder: 'Select a session or create a new one',
});
sendFileSelectMessage(ChatPanel.currentPanel().panel(), uri.path);
});
const disposableCodeContext = vscode.commands.registerCommand('devchat.askForCode', async () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (!ChatPanel.currentPanel()) {
if (vscode.workspace.workspaceFolders) {
ChatPanel.createOrShow(context.extensionUri);
} else {
vscode.window.showErrorMessage('Please open a directory before using the chat panel.');
return
}
}
const selectedText = editor.document.getText(editor.selection);
sendCodeSelectMessage(ChatPanel.currentPanel().panel(), selectedText);
}
});
if (!selectedOption) {
return;
}
const disposableAskFile = vscode.commands.registerCommand('devchat.askForFile', async () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (!ChatPanel.currentPanel()) {
if (vscode.workspace.workspaceFolders) {
ChatPanel.createOrShow(context.extensionUri);
} else {
vscode.window.showErrorMessage('Please open a directory before using the chat panel.');
return
}
}
const selectedText = editor.document.getText();
sendCodeSelectMessage(ChatPanel.currentPanel().panel(), selectedText);
}
});
let sessionName = selectedOption;
if (selectedOption === createNewSessionOption) {
sessionName = await vscode.window.showInputBox({
prompt: 'Enter a new session name',
placeHolder: 'Session Name',
});
if (!sessionName) {
return;
}
}
ChatPanel.createOrShow(context.extensionUri, sessionName);
});
context.subscriptions.push(disposable);
context.subscriptions.push(disposable);
context.subscriptions.push(disposable_add_context);
context.subscriptions.push(disposableCodeContext)
context.subscriptions.push(disposableAskFile)
}
exports.activate = activate;

109
src/messageHandler.ts Normal file
View File

@ -0,0 +1,109 @@
// messageHandler.ts
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { promisify } from 'util';
import DevChat, { LogOptions } from './devchat';
import DtmWrapper from './dtm';
import applyCode, {applyCodeFile} from './applyCode';
import * as vscode3 from 'vscode';
const writeFileAsync = promisify(fs.writeFile);
const unlinkAsync = promisify(fs.unlink);
let lastPromptHash: string | undefined;
async function saveTempPatchFile(content: string): Promise<string> {
const tempPatchFilePath = path.join(vscode.workspace.workspaceFolders![0].uri.fsPath, '.temp_patch_file.patch');
await writeFileAsync(tempPatchFilePath, content);
return tempPatchFilePath;
}
async function deleteTempPatchFile(filePath: string): Promise<void> {
await unlinkAsync(filePath);
}
export function sendFileSelectMessage(panel: vscode.WebviewPanel, filePath: string): void {
panel.webview.postMessage({ command: 'file_select', filePath });
}
export function sendCodeSelectMessage(panel: vscode.WebviewPanel, codeBlock: string): void {
panel.webview.postMessage({ command: 'code_select', codeBlock });
}
export function askAI(panel: vscode.WebviewPanel, codeBlock: string, question: string): void {
panel.webview.postMessage({ command: 'ask_ai', codeBlock, question });
}
// Add this function to messageHandler.ts
function parseMessageForContext(message: string): { context: string[]; text: string } {
const contextRegex = /\[context\|(.*?)\]/g;
const contextPaths = [];
let match;
while ((match = contextRegex.exec(message)) !== null) {
contextPaths.push(match[1]);
}
const text = message.replace(contextRegex, '').trim();
return { context: contextPaths, text };
}
async function handleMessage(
message: any,
panel: vscode.WebviewPanel
): Promise<void> {
const devChat = new DevChat();
const dtmWrapper = new DtmWrapper();
switch (message.command) {
case 'sendMessage':
const parsedMessage = parseMessageForContext(message.text);
const chatOptions: any = lastPromptHash ? { parent: lastPromptHash } : {};
if (parsedMessage.context.length > 0) {
chatOptions.context = parsedMessage.context;
}
let partialData = "";
const onData = (partialResponse: string) => {
partialData += partialResponse;
panel.webview.postMessage({ command: 'receiveMessagePartial', text: partialData });
};
const chatResponse = await devChat.chat(parsedMessage.text, chatOptions, onData);
lastPromptHash = chatResponse["prompt-hash"];
const response = chatResponse.response;
panel.webview.postMessage({ command: 'receiveMessage', text: response });
return;
case 'historyMessages':
const logOptions: LogOptions = message.options || {};
const logEntries = await devChat.log(logOptions);
panel.webview.postMessage({ command: 'loadHistoryMessages', entries: logEntries });
return;
case 'block_apply':
const tempPatchFile = await saveTempPatchFile(message.content);
try {
const patchResult = await dtmWrapper.patch(tempPatchFile);
await deleteTempPatchFile(tempPatchFile);
if (patchResult.status === 0) {
vscode.window.showInformationMessage('Patch applied successfully.');
} else {
vscode.window.showErrorMessage(`Error applying patch: ${patchResult.message} ${patchResult.log}`);
}
} catch (error) {
await deleteTempPatchFile(tempPatchFile);
vscode.window.showErrorMessage(`Error applying patch: ${error}`);
}
return;
case 'code_apply':
await applyCode(message.content);
return;
case 'code_file_apply':
await applyCodeFile(message.content);
return;
}
}
export default handleMessage;

View File

@ -1,50 +0,0 @@
const { Configuration, OpenAIApi } = require("openai");
import * as dotenv from 'dotenv';
import * as path from 'path';
const extensionDir = path.resolve(__dirname, '..');
const envFilePath = path.join(extensionDir, '.env');
const dotenvOutput = dotenv.config({ path: envFilePath });
console.log('dotenv output:', dotenvOutput);
const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
// Set up proxy settings
const openai = new OpenAIApi(configuration);
const HttpsProxyAgent = require('https-proxy-agent')
const HttpProxyAgent = require('http-proxy-agent');
export async function chatWithGPT(prompt: string, session_id: string, messageList: Array<{ role: string; content: string }>): Promise<any[]> {
const fullConversation = [
...messageList,
{
role: 'user',
content: prompt,
},
];
try {
let completion = await openai.createChatCompletion({
model: "gpt-3.5-turbo",
messages: fullConversation,
temperature: 0.9,
max_tokens: 2048,
top_p: 1,
frequency_penalty: 0.0,
presence_penalty: 0.6,
stop: null,
},
{
}
);
return [0, completion.data.choices[0].message.content]
} catch (e) {
console.log("Error:", e.message)
return [-1, "Error while connecting to the GPT model."];
}
}

View File

@ -1,22 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const test_electron_1 = require("@vscode/test-electron");
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to test runner
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite/index');
// Download VS Code, unzip it and run the integration test
await (0, test_electron_1.runTests)({ extensionDevelopmentPath, extensionTestsPath });
}
catch (err) {
console.error('Failed to run tests', err);
process.exit(1);
}
}
main();
//# sourceMappingURL=runTest.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"runTest.js","sourceRoot":"","sources":["runTest.ts"],"names":[],"mappings":";;AAAA,6BAA6B;AAE7B,yDAAiD;AAEjD,KAAK,UAAU,IAAI;IAClB,IAAI;QACH,4DAA4D;QAC5D,yCAAyC;QACzC,MAAM,wBAAwB,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAEnE,0BAA0B;QAC1B,iCAAiC;QACjC,MAAM,kBAAkB,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;QAEpE,0DAA0D;QAC1D,MAAM,IAAA,wBAAQ,EAAC,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,CAAC,CAAC;KACjE;IAAC,OAAO,GAAG,EAAE;QACb,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;QAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;KAChB;AACF,CAAC;AAED,IAAI,EAAE,CAAC"}

View File

@ -1,23 +0,0 @@
import * as path from 'path';
import { runTests } from '@vscode/test-electron';
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to test runner
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite/index');
// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error('Failed to run tests', err);
process.exit(1);
}
}
main();

View File

@ -1,15 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const assert = require("assert");
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
const vscode = require("vscode");
// import * as myExtension from '../../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));
});
});
//# sourceMappingURL=extension.test.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"extension.test.js","sourceRoot":"","sources":["extension.test.ts"],"names":[],"mappings":";;AAAA,iCAAiC;AAEjC,0DAA0D;AAC1D,8CAA8C;AAC9C,iCAAiC;AACjC,kDAAkD;AAElD,KAAK,CAAC,sBAAsB,EAAE,GAAG,EAAE;IAClC,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAAC,kBAAkB,CAAC,CAAC;IAEzD,IAAI,CAAC,aAAa,EAAE,GAAG,EAAE;QACxB,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}

View File

@ -1,15 +0,0 @@
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';
// import * as myExtension from '../../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));
});
});

View File

@ -1,40 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.run = void 0;
const path = require("path");
const Mocha = require("mocha");
const glob = require("glob");
function run() {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
color: true
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
}
else {
c();
}
});
}
catch (err) {
console.error(err);
e(err);
}
});
});
}
exports.run = run;
//# sourceMappingURL=index.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAAA,6BAA6B;AAC7B,+BAA+B;AAC/B,6BAA6B;AAE7B,SAAgB,GAAG;IAClB,wBAAwB;IACxB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC;QACvB,EAAE,EAAE,KAAK;QACT,KAAK,EAAE,IAAI;KACX,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAEhD,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC3B,IAAI,CAAC,eAAe,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YACxD,IAAI,GAAG,EAAE;gBACR,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC;aACd;YAED,8BAA8B;YAC9B,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAE9D,IAAI;gBACH,qBAAqB;gBACrB,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;oBACpB,IAAI,QAAQ,GAAG,CAAC,EAAE;wBACjB,CAAC,CAAC,IAAI,KAAK,CAAC,GAAG,QAAQ,gBAAgB,CAAC,CAAC,CAAC;qBAC1C;yBAAM;wBACN,CAAC,EAAE,CAAC;qBACJ;gBACF,CAAC,CAAC,CAAC;aACH;YAAC,OAAO,GAAG,EAAE;gBACb,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBACnB,CAAC,CAAC,GAAG,CAAC,CAAC;aACP;QACF,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC;AAjCD,kBAiCC"}

View File

@ -1,38 +0,0 @@
import * as path from 'path';
import * as Mocha from 'mocha';
import * as glob from 'glob';
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
color: true
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
console.error(err);
e(err);
}
});
});
}

View File

@ -15,3 +15,4 @@
// "noUnusedParameters": true, /* Report errors on unused parameters. */
}
}