release 0.0.1

This commit is contained in:
bobo.yang 2023-04-27 14:07:46 +08:00
parent ce4634767c
commit 5f154777f6
16 changed files with 721 additions and 182 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*

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,68 @@
// 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
const renderedContent = md.render(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,17 +74,13 @@ 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
initButtonForCodeBlock(codeBlocks);
messageItem.appendChild(senderIcon);
messageItem.appendChild(messageContent);
@ -121,4 +174,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);
});
}

35
package-lock.json generated
View File

@ -13,6 +13,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"
},
"devDependencies": {
@ -20,6 +22,7 @@
"@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",
@ -1477,6 +1480,12 @@
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
"dev": true
},
"node_modules/@types/shell-escape": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@types/shell-escape/-/shell-escape-0.2.1.tgz",
"integrity": "sha512-95hZXmBvwtvsLMPefKT9xquUSAJXsVDUaipyUiYoYi3ZdLhZ3w30w230Ugs96IdoJQb5ECvj0D82Jj/op00qWQ==",
"dev": true
},
"node_modules/@types/stack-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@ -5341,6 +5350,11 @@
}
]
},
"node_modules/quote": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/quote/-/quote-0.4.0.tgz",
"integrity": "sha512-KHp3y3xDjuBhRx+tYKOgzPnVHMRlgpn2rU450GcU4PL24r1H6ls/hfPrxDwX2pvYMlwODHI2l8WwgoV69x5rUQ=="
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -5615,6 +5629,11 @@
"node": ">=8"
}
},
"node_modules/shell-escape": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
"integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw=="
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@ -7570,6 +7589,12 @@
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
"dev": true
},
"@types/shell-escape": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@types/shell-escape/-/shell-escape-0.2.1.tgz",
"integrity": "sha512-95hZXmBvwtvsLMPefKT9xquUSAJXsVDUaipyUiYoYi3ZdLhZ3w30w230Ugs96IdoJQb5ECvj0D82Jj/op00qWQ==",
"dev": true
},
"@types/stack-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@ -10423,6 +10448,11 @@
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true
},
"quote": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/quote/-/quote-0.4.0.tgz",
"integrity": "sha512-KHp3y3xDjuBhRx+tYKOgzPnVHMRlgpn2rU450GcU4PL24r1H6ls/hfPrxDwX2pvYMlwODHI2l8WwgoV69x5rUQ=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -10622,6 +10652,11 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"shell-escape": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
"integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw=="
},
"signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",

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",
@ -43,6 +78,7 @@
"@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",
@ -63,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"
}
}

View File

@ -1,38 +0,0 @@
import DevChat from "../devchat";
describe("DevChat", () => {
const devChat = new DevChat();
test("some test", () => {
const xx = 20;
expect(xx)
})
test("chat should return a valid response", async () => {
const chatResponse = await devChat.chat("Help me with TypeScript");
expect(chatResponse).toHaveProperty("prompt-hash");
expect(chatResponse).toHaveProperty("user");
expect(chatResponse).toHaveProperty("date");
expect(chatResponse).toHaveProperty("response");
expect(chatResponse).toHaveProperty("isError");
expect(chatResponse.isError).toBe(false);
expect(chatResponse.response.length).toBeGreaterThan(0);
});
test("log should return an array of log entries", async () => {
const logEntries = await devChat.log({ maxCount: 5 });
expect(Array.isArray(logEntries)).toBe(true);
expect(logEntries.length).toBeLessThanOrEqual(5);
logEntries.forEach((entry) => {
expect(entry).toHaveProperty("prompt-hash");
expect(entry).toHaveProperty("user");
expect(entry).toHaveProperty("date");
expect(entry).toHaveProperty("message");
expect(entry).toHaveProperty("response");
});
});
});

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

@ -9,7 +9,6 @@ import handleMessage from './messageHandler';
export default class ChatPanel {
private static _instance: ChatPanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private _messageHistory: Array<{ role: string; content: string }>;
private _disposables: vscode.Disposable[] = [];
// Create or reveal the chat panel
@ -22,6 +21,10 @@ export default class ChatPanel {
}
}
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
@ -35,13 +38,13 @@ export default class ChatPanel {
{
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(extensionUri, 'media')],
retainContextWhenHidden: true
}
);
}
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
this._panel = panel;
this._messageHistory = [];
this.setWebviewOptions(extensionUri);
this.setWebviewContent(extensionUri);
@ -61,6 +64,10 @@ export default class ChatPanel {
this._panel.webview.html = this._getHtmlContent(extensionUri);
}
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);

View File

@ -1,21 +1,53 @@
import { exec } from "child_process";
// 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 execAsync = promisify(exec);
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 = '';
interface ChatOptions {
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[];
}
interface LogOptions {
export interface LogOptions {
skip?: number;
maxCount?: number;
}
interface LogEntry {
export interface LogEntry {
"prompt-hash": string;
user: string;
date: string;
@ -23,7 +55,7 @@ interface LogEntry {
response: string;
}
interface ChatResponse {
export interface ChatResponse {
"prompt-hash": string;
user: string;
date: string;
@ -32,25 +64,47 @@ interface ChatResponse {
}
class DevChat {
async chat(content: string, options: ChatOptions = {}): Promise<ChatResponse> {
let args = "";
if (options.parent) {
args += ` -p ${options.parent}`;
}
if (options.reference) {
args += ` -r ${options.reference.join(",")}`;
}
if (options.header) {
args += ` -h ${options.header.join(",")}`;
}
if (options.context) {
args += ` -c ${options.context.join(",")}`;
}
const { stdout } = await execAsync(`devchat ${args} "${content}"`, {
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)
@ -63,8 +117,17 @@ class DevChat {
isError: true,
};
}
if (responseLines[0].startsWith("error")) {
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: "",
@ -73,18 +136,17 @@ class DevChat {
isError: true,
};
}
const promptHashLine = responseLines.shift()!;
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,
@ -92,22 +154,50 @@ class DevChat {
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";
let args = ["log"];
if (options.skip) {
args += ` --skip ${options.skip}`;
args.push('--skip', `${options.skip}`);
}
if (options.maxCount) {
args += ` --max-count ${options.maxCount}`;
args.push('--max-count', `${options.maxCount}`);
}
const { stdout } = await execAsync(`devchat ${args}`, {
maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB
});
return JSON.parse(stdout.trim());
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 [];
}
}
}

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,5 +1,9 @@
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', () => {
@ -10,6 +14,56 @@ function activate(context: { extensionUri: any; subscriptions: any[]; }) {
}
});
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
}
}
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);
}
});
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);
}
});
context.subscriptions.push(disposable);
context.subscriptions.push(disposable_add_context);
context.subscriptions.push(disposableCodeContext)
context.subscriptions.push(disposableAskFile)
}
exports.activate = activate;

View File

@ -1,19 +1,108 @@
// messageHandler.ts
import * as vscode from 'vscode';
import DevChat from './devchat';
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 devChat = new DevChat();
const chatResponse = await devChat.chat(message.text);
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;
}
}