commit
9a4840c514
6
.mocharc.json
Normal file
6
.mocharc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "test/**/*.test.ts",
|
||||
"require": "ts-node/register",
|
||||
"project": "tsconfig.test.json"
|
||||
}
|
1590
package-lock.json
generated
1590
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@ -2,7 +2,7 @@
|
||||
"name": "devchat",
|
||||
"displayName": "DevChat",
|
||||
"description": "Write prompts, not code",
|
||||
"version": "0.0.45",
|
||||
"version": "0.0.47",
|
||||
"icon": "assets/devchat.png",
|
||||
"publisher": "merico",
|
||||
"engines": {
|
||||
@ -155,7 +155,7 @@
|
||||
},
|
||||
{
|
||||
"command": "DevChat.OPENAI_API_KEY",
|
||||
"title": "DEVCHAT_API_KEY",
|
||||
"title": "Input Access Key",
|
||||
"category": "DevChat"
|
||||
},
|
||||
{
|
||||
@ -314,7 +314,7 @@
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
"lint": "eslint src --ext ts",
|
||||
"test": "node ./out/test/runTest.js",
|
||||
"test": "mocha",
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"dev": "webpack serve --config webpack.config.js --open"
|
||||
},
|
||||
@ -323,19 +323,24 @@
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.21.5",
|
||||
"@types/chai": "^4.3.5",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/jest": "^29.5.1",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/ncp": "^2.0.5",
|
||||
"@types/node": "16.x",
|
||||
"@types/proxyquire": "^1.3.28",
|
||||
"@types/react-dom": "^18.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"@types/shell-escape": "^0.2.1",
|
||||
"@types/sinon": "^10.0.15",
|
||||
"@types/uuid": "^9.0.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",
|
||||
"babel-loader": "^9.1.2",
|
||||
"chai": "^4.3.7",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"dotenv": "^16.0.3",
|
||||
@ -345,15 +350,21 @@
|
||||
"html-webpack-plugin": "^5.5.1",
|
||||
"jest": "^29.5.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"mocha": "^10.2.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"proxyquire": "^2.1.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"redux": "^4.2.1",
|
||||
"sinon": "^15.1.0",
|
||||
"style-loader": "^3.3.2",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5",
|
||||
"url-loader": "^4.1.1",
|
||||
"vscode-test": "^1.6.1",
|
||||
"webpack": "^5.76.3",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.13.3"
|
||||
|
@ -1,8 +1,4 @@
|
||||
import { vs } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import CustomCommands from "./customCommand";
|
||||
import { logger } from "../util/logger";
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface Command {
|
||||
name: string;
|
||||
|
@ -2,88 +2,92 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { logger } from '../util/logger';
|
||||
|
||||
interface Command {
|
||||
name: string;
|
||||
pattern: string;
|
||||
description: string;
|
||||
message: string;
|
||||
default: boolean;
|
||||
show: boolean;
|
||||
instructions: string[];
|
||||
export interface Command {
|
||||
name: string;
|
||||
pattern: string;
|
||||
description: string;
|
||||
message: string;
|
||||
default: boolean;
|
||||
show: boolean;
|
||||
instructions: string[];
|
||||
}
|
||||
|
||||
class CustomCommands {
|
||||
private static instance: CustomCommands | null = null;
|
||||
private commands: Command[] = [];
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
public static getInstance(): CustomCommands {
|
||||
if (!CustomCommands.instance) {
|
||||
CustomCommands.instance = new CustomCommands();
|
||||
}
|
||||
return CustomCommands.instance;
|
||||
}
|
||||
|
||||
public parseCommands(workflowsDir: string): void {
|
||||
this.commands = [];
|
||||
|
||||
try {
|
||||
const subDirs = fs.readdirSync(workflowsDir, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
|
||||
for (const dir of subDirs) {
|
||||
const settingsPath = path.join(workflowsDir, dir, '_setting_.json');
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
||||
const command: Command = {
|
||||
name: dir,
|
||||
pattern: settings.pattern,
|
||||
description: settings.description,
|
||||
message: settings.message,
|
||||
default: settings.default,
|
||||
show: settings.show === undefined ? "true": settings.show,
|
||||
instructions: settings.instructions
|
||||
};
|
||||
this.commands.push(command);
|
||||
public static getInstance(): CustomCommands {
|
||||
if (!CustomCommands.instance) {
|
||||
CustomCommands.instance = new CustomCommands();
|
||||
}
|
||||
return CustomCommands.instance;
|
||||
}
|
||||
|
||||
public parseCommands(workflowsDir: string): void {
|
||||
this.commands = [];
|
||||
|
||||
try {
|
||||
const subDirs = fs.readdirSync(workflowsDir, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
|
||||
for (const dir of subDirs) {
|
||||
const settingsPath = path.join(workflowsDir, dir, '_setting_.json');
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
||||
const command: Command = {
|
||||
name: dir,
|
||||
pattern: settings.pattern,
|
||||
description: settings.description,
|
||||
message: settings.message,
|
||||
default: settings.default,
|
||||
show: settings.show === undefined ? "true" : settings.show,
|
||||
instructions: settings.instructions
|
||||
};
|
||||
this.commands.push(command);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 显示错误消息
|
||||
logger.channel()?.error(`Failed to parse commands: ${error}`);
|
||||
logger.channel()?.show();
|
||||
}
|
||||
} catch (error) {
|
||||
// 显示错误消息
|
||||
logger.channel()?.error(`Failed to parse commands: ${error}`);
|
||||
logger.channel()?.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getCommands(): Command[] {
|
||||
return this.commands;
|
||||
}
|
||||
public regCommand(command: Command) {
|
||||
this.commands.push(command);
|
||||
}
|
||||
|
||||
public getCommand(commandName: string): Command | null {
|
||||
const foundCommand = this.commands.find(command => command.name === commandName);
|
||||
return foundCommand ? foundCommand : null;
|
||||
public getCommands(): Command[] {
|
||||
return this.commands;
|
||||
}
|
||||
|
||||
public getCommand(commandName: string): Command | null {
|
||||
const foundCommand = this.commands.find(command => command.name === commandName);
|
||||
return foundCommand ? foundCommand : null;
|
||||
}
|
||||
|
||||
|
||||
public handleCommand(commandName: string): string {
|
||||
// 获取命令对象,这里假设您已经有一个方法或属性可以获取到命令对象
|
||||
const command = this.getCommand(commandName);
|
||||
if (!command) {
|
||||
logger.channel()?.error(`Command ${commandName} not found!`);
|
||||
logger.channel()?.show();
|
||||
return '';
|
||||
public handleCommand(commandName: string): string {
|
||||
// 获取命令对象,这里假设您已经有一个方法或属性可以获取到命令对象
|
||||
const command = this.getCommand(commandName);
|
||||
if (!command) {
|
||||
logger.channel()?.error(`Command ${commandName} not found!`);
|
||||
logger.channel()?.show();
|
||||
return '';
|
||||
}
|
||||
|
||||
// 构建instructions列表字符串
|
||||
const instructions = command!.instructions
|
||||
.map((instruction: string) => `[instruction|./.chat/workflows/${command.name}/${instruction}]`)
|
||||
.join(' ');
|
||||
|
||||
// 返回结果字符串
|
||||
return `${instructions} ${command!.message}`;
|
||||
}
|
||||
|
||||
// 构建instructions列表字符串
|
||||
const instructions = command!.instructions
|
||||
.map((instruction: string) => `[instruction|./.chat/workflows/${command.name}/${instruction}]`)
|
||||
.join(' ');
|
||||
|
||||
// 返回结果字符串
|
||||
return `${instructions} ${command!.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomCommands;
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { createTempSubdirectory, getLanguageIdByFileName } from '../util/commonUtil';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
export async function handleCodeSelected(fileSelected: string, codeSelected: string) {
|
||||
// get file name from fileSelected
|
||||
@ -15,7 +15,7 @@ export async function handleCodeSelected(fileSelected: string, codeSelected: str
|
||||
const languageId = await getLanguageIdByFileName(fileSelected);
|
||||
|
||||
// get relative path of workspace
|
||||
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
|
||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
const relativePath = path.relative(workspaceDir!, fileSelected);
|
||||
|
||||
// convert fileContent to markdown code block with languageId and file path
|
||||
@ -27,7 +27,7 @@ export async function handleCodeSelected(fileSelected: string, codeSelected: str
|
||||
const jsonData = JSON.stringify(data);
|
||||
|
||||
// save markdownCodeBlock to temp file
|
||||
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFile), Buffer.from(jsonData));
|
||||
await UiUtilWrapper.writeFile(tempFile, jsonData);
|
||||
|
||||
return `[context|${tempFile}]`;
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { ChatContext } from './contextManager';
|
||||
import { createTempSubdirectory, runCommandStringAndWriteOutput } from '../util/commonUtil';
|
||||
import { logger } from '../util/logger';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
|
||||
export const customCommandContext: ChatContext = {
|
||||
name: '<custom command>',
|
||||
description: 'custorm command',
|
||||
handler: async () => {
|
||||
// popup a dialog to ask for the command line to run
|
||||
const customCommand = await vscode.window.showInputBox({
|
||||
const customCommand = await UiUtilWrapper.showInputBox({
|
||||
prompt: 'Input your custom command',
|
||||
placeHolder: 'for example: ls -l'
|
||||
});
|
||||
@ -17,15 +18,15 @@ export const customCommandContext: ChatContext = {
|
||||
// 检查用户是否输入了命令
|
||||
if (customCommand) {
|
||||
const tempDir = await createTempSubdirectory('devchat/context');
|
||||
const diff_file = path.join(tempDir, 'custom.txt');
|
||||
const diffFile = path.join(tempDir, 'custom.txt');
|
||||
|
||||
logger.channel()?.info(`custom command: ${customCommand}`);
|
||||
const result = await runCommandStringAndWriteOutput(customCommand, diff_file);
|
||||
const result = await runCommandStringAndWriteOutput(customCommand, diffFile);
|
||||
logger.channel()?.info(`custom command: ${customCommand} exit code:`, result.exitCode);
|
||||
|
||||
logger.channel()?.debug(`custom command: ${customCommand} stdout:`, result.stdout);
|
||||
logger.channel()?.debug(`custom command: ${customCommand} stderr:`, result.stderr);
|
||||
return `[context|${diff_file}]`;
|
||||
return `[context|${diffFile}]`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
@ -1,8 +1,8 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { createTempSubdirectory, getLanguageIdByFileName } from '../util/commonUtil';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
export async function handleFileSelected(fileSelected: string) {
|
||||
// get file name from fileSelected
|
||||
@ -18,7 +18,7 @@ export async function handleFileSelected(fileSelected: string) {
|
||||
const languageId = await getLanguageIdByFileName(fileSelected);
|
||||
|
||||
// get relative path of workspace
|
||||
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
|
||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
const relativePath = path.relative(workspaceDir!, fileSelected);
|
||||
|
||||
// convert fileContent to markdown code block with languageId and file path
|
||||
@ -30,7 +30,7 @@ export async function handleFileSelected(fileSelected: string) {
|
||||
const jsonData = JSON.stringify(data);
|
||||
|
||||
// save markdownCodeBlock to temp file
|
||||
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFile), Buffer.from(jsonData));
|
||||
await UiUtilWrapper.writeFile(tempFile, jsonData);
|
||||
|
||||
return `[context|${tempFile}]`;
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { createTempSubdirectory, runCommandStringAndWriteOutput } from '../util/commonUtil';
|
||||
import { logger } from '../util/logger';
|
||||
|
@ -1,186 +1,184 @@
|
||||
import * as vscode from 'vscode';
|
||||
import ChatPanel from '../panel/chatPanel';
|
||||
import { sendFileSelectMessage, sendCodeSelectMessage } from './util';
|
||||
import { logger } from '../util/logger';
|
||||
import * as childProcess from 'child_process';
|
||||
import { DevChatViewProvider } from '../panel/devchatView';
|
||||
import ExtensionContextHolder from '../util/extensionContext';
|
||||
import { TopicManager } from '../topic/topicManager';
|
||||
import { TopicTreeDataProvider, TopicTreeItem } from '../panel/topicView';
|
||||
import { FilePairManager } from '../util/diffFilePairs';
|
||||
import { ApiKeyManager } from '../util/apiKey';
|
||||
|
||||
import * as process from 'process';
|
||||
|
||||
export function checkDevChatDependency(): boolean {
|
||||
try {
|
||||
// Get pipx environment
|
||||
const pipxEnvOutput = childProcess.execSync('python3 -m pipx environment').toString();
|
||||
const binPathRegex = /PIPX_BIN_DIR=\s*(.*)/;
|
||||
|
||||
// Get BIN path from pipx environment
|
||||
const match = pipxEnvOutput.match(binPathRegex);
|
||||
if (match && match[1]) {
|
||||
const binPath = match[1];
|
||||
|
||||
// Add BIN path to PATH
|
||||
process.env.PATH = `${binPath}:${process.env.PATH}`;
|
||||
|
||||
// Check if DevChat is installed
|
||||
childProcess.execSync('devchat --help');
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
// DevChat dependency check failed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkOpenaiApiKey() {
|
||||
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets;
|
||||
let openaiApiKey = await secretStorage.get("devchat_OPENAI_API_KEY");
|
||||
if (!openaiApiKey) {
|
||||
openaiApiKey = vscode.workspace.getConfiguration('DevChat').get('API_KEY');
|
||||
}
|
||||
if (!openaiApiKey) {
|
||||
openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
}
|
||||
if (!openaiApiKey) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkOpenaiKey() {
|
||||
let openaiApiKey = vscode.workspace.getConfiguration('DevChat').get('API_KEY');
|
||||
if (!openaiApiKey) {
|
||||
openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
}
|
||||
if (!openaiApiKey) {
|
||||
// OpenAI key not set
|
||||
vscode.window.showInputBox({
|
||||
placeHolder: 'Please input your OpenAI API key (or DevChat access key)'
|
||||
}).then((value) => {
|
||||
if (value) {
|
||||
// 设置用户输入的API Key
|
||||
vscode.workspace.getConfiguration('DevChat').update('API_KEY', value, true);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkDependencyPackage() {
|
||||
const dependencyInstalled = checkDevChatDependency();
|
||||
if (!dependencyInstalled) {
|
||||
// Prompt the user, whether to install devchat using pip3 install devchat
|
||||
const installPrompt = 'devchat is not installed. Do you want to install it using pip3 install devchat?';
|
||||
const installAction = 'Install';
|
||||
|
||||
vscode.window.showInformationMessage(installPrompt, installAction).then((selectedAction) => {
|
||||
if (selectedAction === installAction) {
|
||||
// Install devchat using pip3 install devchat
|
||||
const terminal = vscode.window.createTerminal("DevChat Install");
|
||||
terminal.sendText("pip3 install --upgrade devchat");
|
||||
terminal.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!checkOpenaiKey()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function registerOpenChatPanelCommand(context: vscode.ExtensionContext) {
|
||||
let disposable = vscode.commands.registerCommand('devchat.openChatPanel',async () => {
|
||||
let disposable = vscode.commands.registerCommand('devchat.openChatPanel', async () => {
|
||||
await vscode.commands.executeCommand('devchat-view.focus');
|
||||
});
|
||||
context.subscriptions.push(disposable);
|
||||
});
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
async function ensureChatPanel(context: vscode.ExtensionContext): Promise<boolean> {
|
||||
await vscode.commands.executeCommand('devchat-view.focus');
|
||||
return true;
|
||||
await vscode.commands.executeCommand('devchat-view.focus');
|
||||
return true;
|
||||
}
|
||||
|
||||
function registerAddContextCommand(context: vscode.ExtensionContext) {
|
||||
const disposableAddContext = vscode.commands.registerCommand('devchat.addConext', async (uri: { path: any; }) => {
|
||||
if (!await ensureChatPanel(context)) {
|
||||
return;
|
||||
}
|
||||
const callback = async (uri: { path: any; }) => {
|
||||
if (!await ensureChatPanel(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, uri.path);
|
||||
});
|
||||
context.subscriptions.push(disposableAddContext);
|
||||
|
||||
const disposableAddContextChinese = vscode.commands.registerCommand('devchat.addConext_chinese', async (uri: { path: any; }) => {
|
||||
if (!await ensureChatPanel(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, uri.path);
|
||||
});
|
||||
context.subscriptions.push(disposableAddContextChinese);
|
||||
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, uri.path);
|
||||
};
|
||||
context.subscriptions.push(vscode.commands.registerCommand('devchat.addConext', callback));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('devchat.addConext_chinese', callback));
|
||||
}
|
||||
|
||||
function registerAskForCodeCommand(context: vscode.ExtensionContext) {
|
||||
const disposableCodeContext = vscode.commands.registerCommand('devchat.askForCode', async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
if (!await ensureChatPanel(context)) {
|
||||
return;
|
||||
}
|
||||
const callback = async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
if (!await ensureChatPanel(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedText = editor.document.getText(editor.selection);
|
||||
await sendCodeSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName, selectedText);
|
||||
}
|
||||
});
|
||||
context.subscriptions.push(disposableCodeContext);
|
||||
|
||||
const disposableCodeContextChinese = vscode.commands.registerCommand('devchat.askForCode_chinese', async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
if (!await ensureChatPanel(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedText = editor.document.getText(editor.selection);
|
||||
await sendCodeSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName, selectedText);
|
||||
}
|
||||
});
|
||||
context.subscriptions.push(disposableCodeContextChinese);
|
||||
const selectedText = editor.document.getText(editor.selection);
|
||||
await sendCodeSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName, selectedText);
|
||||
}
|
||||
};
|
||||
context.subscriptions.push(vscode.commands.registerCommand('devchat.askForCode', callback));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('devchat.askForCode_chinese', callback));
|
||||
}
|
||||
|
||||
function registerAskForFileCommand(context: vscode.ExtensionContext) {
|
||||
const disposableAskFile = vscode.commands.registerCommand('devchat.askForFile', async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
if (!await ensureChatPanel(context)) {
|
||||
return;
|
||||
}
|
||||
const callback = async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
if (!await ensureChatPanel(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName);
|
||||
}
|
||||
});
|
||||
context.subscriptions.push(disposableAskFile);
|
||||
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName);
|
||||
}
|
||||
};
|
||||
context.subscriptions.push(vscode.commands.registerCommand('devchat.askForFile', callback));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('devchat.askForFile_chinese', callback));
|
||||
}
|
||||
|
||||
const disposableAskFileChinese = vscode.commands.registerCommand('devchat.askForFile_chinese', async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
if (!await ensureChatPanel(context)) {
|
||||
return;
|
||||
}
|
||||
export function registerApiKeySettingCommand(context: vscode.ExtensionContext) {
|
||||
const secretStorage: vscode.SecretStorage = context.secrets;
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('DevChat.OPENAI_API_KEY', async () => {
|
||||
const passwordInput: string = await vscode.window.showInputBox({
|
||||
password: true,
|
||||
title: "Input Access Key",
|
||||
placeHolder: "Set OPENAI_API_KEY (or DevChat Access Key)"
|
||||
}) ?? '';
|
||||
|
||||
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName);
|
||||
}
|
||||
});
|
||||
context.subscriptions.push(disposableAskFileChinese);
|
||||
ApiKeyManager.writeApiKeySecret(passwordInput);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function registerStatusBarItemClickCommand(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('devcaht.onStatusBarClick', async () => {
|
||||
await vscode.commands.executeCommand('devchat-view.focus');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const topicDeleteCallback = async (item: TopicTreeItem) => {
|
||||
const confirm = 'Delete';
|
||||
const cancel = 'Cancel';
|
||||
const label = typeof item.label === 'string' ? item.label : item.label!.label;
|
||||
const truncatedLabel = label.substring(0, 20) + (label.length > 20 ? '...' : '');
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
`Are you sure you want to delete the topic "${truncatedLabel}"?`,
|
||||
{ modal: true },
|
||||
confirm,
|
||||
cancel
|
||||
);
|
||||
|
||||
if (result === confirm) {
|
||||
TopicManager.getInstance().deleteTopic(item.id);
|
||||
}
|
||||
};
|
||||
;
|
||||
|
||||
export function regTopicDeleteCommand(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('devchat-topicview.deleteTopic', topicDeleteCallback)
|
||||
);
|
||||
}
|
||||
|
||||
export function regAddTopicCommand(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('devchat-topicview.addTopic', () => {
|
||||
const topic = TopicManager.getInstance().createTopic();
|
||||
TopicManager.getInstance().setCurrentTopic(topic.topicId);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function regDeleteSelectTopicCommand(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('devchat-topicview.deleteSelectedTopic', () => {
|
||||
const selectedItem = TopicTreeDataProvider.getInstance().selectedItem;
|
||||
if (selectedItem) {
|
||||
topicDeleteCallback(selectedItem);
|
||||
} else {
|
||||
vscode.window.showErrorMessage('No item selected');
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function regSelectTopicCommand(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('devchat-topicview.selectTopic', (item: TopicTreeItem) => {
|
||||
TopicTreeDataProvider.getInstance().setSelectedItem(item);
|
||||
TopicManager.getInstance().setCurrentTopic(item.id);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function regReloadTopicCommand(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('devchat-topicview.reloadTopic', async () => {
|
||||
TopicManager.getInstance().loadTopics();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function regApplyDiffResultCommand(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('devchat.applyDiffResult', async () => {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
const fileName = activeEditor!.document.fileName;
|
||||
|
||||
const [leftUri, rightUri] = FilePairManager.getInstance().findPair(fileName) || [undefined, undefined];
|
||||
if (leftUri && rightUri) {
|
||||
// 获取对比的两个文件
|
||||
const leftDoc = await vscode.workspace.openTextDocument(leftUri);
|
||||
const rightDoc = await vscode.workspace.openTextDocument(rightUri);
|
||||
|
||||
// 将右边文档的内容替换到左边文档
|
||||
const leftEditor = await vscode.window.showTextDocument(leftDoc);
|
||||
await leftEditor.edit(editBuilder => {
|
||||
const fullRange = new vscode.Range(0, 0, leftDoc.lineCount, 0);
|
||||
editBuilder.replace(fullRange, rightDoc.getText());
|
||||
});
|
||||
|
||||
// 保存左边文档
|
||||
await leftDoc.save();
|
||||
} else {
|
||||
vscode.window.showErrorMessage('No file to apply diff result.');
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
checkDependencyPackage,
|
||||
registerOpenChatPanelCommand,
|
||||
registerAddContextCommand,
|
||||
registerAskForCodeCommand,
|
||||
registerAskForFileCommand,
|
||||
registerOpenChatPanelCommand,
|
||||
registerAddContextCommand,
|
||||
registerAskForCodeCommand,
|
||||
registerAskForFileCommand,
|
||||
};
|
||||
|
42
src/contributes/commandsBase.ts
Normal file
42
src/contributes/commandsBase.ts
Normal file
@ -0,0 +1,42 @@
|
||||
// src/contributes/commandsBase.ts
|
||||
|
||||
import { runCommand } from "../util/commonUtil";
|
||||
|
||||
|
||||
export function checkDevChatDependency(): boolean {
|
||||
try {
|
||||
const binPath = getPipxEnvironmentPath();
|
||||
|
||||
if (binPath) {
|
||||
updateEnvironmentPath(binPath);
|
||||
|
||||
// Check if DevChat is installed
|
||||
runCommand('devchat --help');
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
// DevChat dependency check failed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPipxEnvironmentPath(): string | null {
|
||||
// Get pipx environment
|
||||
const pipxEnvOutput = runCommand('python3 -m pipx environment').toString();
|
||||
const binPathRegex = /PIPX_BIN_DIR=\s*(.*)/;
|
||||
|
||||
// Get BIN path from pipx environment
|
||||
const match = pipxEnvOutput.match(binPathRegex);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateEnvironmentPath(binPath: string): void {
|
||||
// Add BIN path to PATH
|
||||
process.env.PATH = `${binPath}:${process.env.PATH}`;
|
||||
}
|
10
src/contributes/context.ts
Normal file
10
src/contributes/context.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function regLanguageContext() {
|
||||
const currentLocale = vscode.env.language;
|
||||
if (currentLocale === 'zh-cn' || currentLocale === 'zh-tw') {
|
||||
vscode.commands.executeCommand('setContext', 'isChineseLocale', true);
|
||||
} else {
|
||||
vscode.commands.executeCommand('setContext', 'isChineseLocale', false);
|
||||
}
|
||||
}
|
@ -4,15 +4,18 @@ import { handleCodeSelected } from '../context/contextCodeSelected';
|
||||
import { handleFileSelected } from '../context/contextFileSelected';
|
||||
import { MessageHandler } from '../handler/messageHandler';
|
||||
import { regInMessage, regOutMessage } from '../util/reg_messages';
|
||||
import { logger } from '../util/logger';
|
||||
|
||||
regOutMessage({command: 'appendContext', context: ''});
|
||||
export async function sendFileSelectMessage(panel: vscode.WebviewPanel|vscode.WebviewView, filePath: string): Promise<void> {
|
||||
logger.channel()?.info(`Append context: ${filePath}`);
|
||||
const codeContext = await handleFileSelected(filePath);
|
||||
MessageHandler.sendMessage(panel, { command: 'appendContext', context: codeContext });
|
||||
}
|
||||
|
||||
regOutMessage({command: 'appendContext', context: ''});
|
||||
export async function sendCodeSelectMessage(panel: vscode.WebviewPanel|vscode.WebviewView, filePath: string, codeBlock: string): Promise<void> {
|
||||
logger.channel()?.info(`Append context: ${filePath}`);
|
||||
const codeContext = await handleCodeSelected(filePath, codeBlock);
|
||||
MessageHandler.sendMessage(panel, { command: 'appendContext', context: codeContext });
|
||||
}
|
21
src/contributes/views.ts
Normal file
21
src/contributes/views.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { DevChatViewProvider } from '../panel/devchatView';
|
||||
import { TopicTreeDataProvider } from '../panel/topicView';
|
||||
import ExtensionContextHolder from '../util/extensionContext';
|
||||
|
||||
|
||||
export function regDevChatView(context: vscode.ExtensionContext) {
|
||||
ExtensionContextHolder.provider = new DevChatViewProvider(context);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider('devchat-view', ExtensionContextHolder.provider, {
|
||||
webviewOptions: { retainContextWhenHidden: true }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function regTopicView(context: vscode.ExtensionContext) {
|
||||
const yourTreeView = vscode.window.createTreeView('devchat-topicview', {
|
||||
treeDataProvider: TopicTreeDataProvider.getInstance(),
|
||||
});
|
||||
context.subscriptions.push(yourTreeView);
|
||||
}
|
359
src/extension.ts
359
src/extension.ts
@ -1,358 +1,55 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import {
|
||||
checkOpenaiApiKey,
|
||||
checkDevChatDependency,
|
||||
checkDependencyPackage,
|
||||
registerOpenChatPanelCommand,
|
||||
registerAddContextCommand,
|
||||
registerAskForCodeCommand,
|
||||
registerAskForFileCommand,
|
||||
registerApiKeySettingCommand,
|
||||
regTopicDeleteCommand,
|
||||
regAddTopicCommand,
|
||||
regDeleteSelectTopicCommand,
|
||||
regSelectTopicCommand,
|
||||
regReloadTopicCommand,
|
||||
regApplyDiffResultCommand,
|
||||
registerStatusBarItemClickCommand,
|
||||
} from './contributes/commands';
|
||||
import { regLanguageContext } from './contributes/context';
|
||||
import { regDevChatView, regTopicView } from './contributes/views';
|
||||
|
||||
import ExtensionContextHolder from './util/extensionContext';
|
||||
import { logger } from './util/logger';
|
||||
import { DevChatViewProvider } from './panel/devchatView';
|
||||
import path from 'path';
|
||||
import { FilePairManager } from './util/diffFilePairs';
|
||||
import { Topic, TopicManager } from './topic/topicManager';
|
||||
|
||||
|
||||
class TopicTreeItem extends vscode.TreeItem {
|
||||
id: string;
|
||||
date: number | undefined;
|
||||
constructor(label: string, id: string, date: number | undefined, collapsibleState: vscode.TreeItemCollapsibleState) {
|
||||
super(label, collapsibleState);
|
||||
this.id = id;
|
||||
this.date = date;
|
||||
this.iconPath = new vscode.ThemeIcon('symbol-variable');
|
||||
this.contextValue = 'yourTreeItem'; // 添加这一行
|
||||
}
|
||||
}
|
||||
|
||||
class TopicTreeDataProvider implements vscode.TreeDataProvider<TopicTreeItem> {
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<TopicTreeItem | undefined | null | void> = new vscode.EventEmitter<TopicTreeItem | undefined | null | void>();
|
||||
readonly onDidChangeTreeData: vscode.Event<TopicTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
|
||||
|
||||
public selectedItem: TopicTreeItem | null = null;
|
||||
private items: TopicTreeItem[] = [];
|
||||
|
||||
// reg listeners to TopicManager in constructor
|
||||
constructor() {
|
||||
TopicManager.getInstance().addOnCreateTopicListener(this.addItem.bind(this));
|
||||
TopicManager.getInstance().addOnDeleteTopicListener(this.onDeleteTopic.bind(this));
|
||||
TopicManager.getInstance().addOnReloadTopicsListener(this.onReloadTopics.bind(this));
|
||||
TopicManager.getInstance().addOnUpdateTopicListener(this.onUpdateTopics.bind(this));
|
||||
}
|
||||
|
||||
// sort items
|
||||
private sortItems() {
|
||||
this.items.sort((a, b) => {
|
||||
if (a.date && b.date) {
|
||||
return b.date - a.date;
|
||||
} else if (!a.date) {
|
||||
return -1;
|
||||
} else if (!b.date) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onUpdateTopics(topicId: string) {
|
||||
const items = this.items.filter(i => i.id === topicId);
|
||||
const topic = TopicManager.getInstance().getTopic(topicId);
|
||||
items.map((item) => {
|
||||
item.label = topic?.name;
|
||||
item.date = topic?.lastUpdated;
|
||||
});
|
||||
this.sortItems();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
onReloadTopics(topics: Topic[]) {
|
||||
const items = topics.map((topic) => {
|
||||
return new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None);
|
||||
});
|
||||
this.items = items;
|
||||
this.sortItems();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
onDeleteTopic(topicId: string) {
|
||||
this.items = this.items.filter(i => i.id !== topicId);
|
||||
this.sortItems();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
setSelectedItem(item: TopicTreeItem): void {
|
||||
this.selectedItem = item;
|
||||
}
|
||||
|
||||
getChildren(element?: TopicTreeItem): vscode.ProviderResult<TopicTreeItem[]> {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
getTreeItem(element: TopicTreeItem): vscode.TreeItem | Thenable<vscode.TreeItem> {
|
||||
element.command = {
|
||||
title: 'Select Item',
|
||||
command: 'devchat-topicview.selectTopic',
|
||||
arguments: [element],
|
||||
};
|
||||
return element;
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
const topicList = TopicManager.getInstance().getTopicList();
|
||||
this.onReloadTopics(topicList);
|
||||
}
|
||||
|
||||
addItem(topic: Topic): void {
|
||||
const newItem = new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None);
|
||||
this.items.push(newItem);
|
||||
this.sortItems();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
|
||||
deleteItem(item: TopicTreeItem): void {
|
||||
this.items = this.items.filter(i => i !== item);
|
||||
this.sortItems();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
}
|
||||
|
||||
function getExtensionVersion(context: vscode.ExtensionContext): string {
|
||||
const packageJsonPath = path.join(context.extensionUri.fsPath, 'package.json');
|
||||
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
|
||||
const packageJson = JSON.parse(packageJsonContent);
|
||||
|
||||
return packageJson.version;
|
||||
}
|
||||
import { LoggerChannelVscode } from './util/logger_vscode';
|
||||
import { createStatusBarItem } from './panel/statusBarView';
|
||||
import { UiUtilWrapper } from './util/uiUtil';
|
||||
import { UiUtilVscode } from './util/uiUtil_vscode';
|
||||
|
||||
|
||||
function activate(context: vscode.ExtensionContext) {
|
||||
ExtensionContextHolder.context = context;
|
||||
const extensionVersion = getExtensionVersion(context);
|
||||
logger.init(context);
|
||||
|
||||
const secretStorage: vscode.SecretStorage = context.secrets;
|
||||
vscode.commands.registerCommand('DevChat.OPENAI_API_KEY', async () => {
|
||||
const passwordInput: string = await vscode.window.showInputBox({
|
||||
password: true,
|
||||
title: "OPENAI_API_KEY"
|
||||
}) ?? '';
|
||||
logger.init(LoggerChannelVscode.getInstance());
|
||||
UiUtilWrapper.init(new UiUtilVscode());
|
||||
|
||||
secretStorage.store("devchat_OPENAI_API_KEY", passwordInput);
|
||||
});
|
||||
regLanguageContext();
|
||||
|
||||
const currentLocale = vscode.env.language;
|
||||
if (currentLocale === 'zh-cn' || currentLocale === 'zh-tw') {
|
||||
vscode.commands.executeCommand('setContext', 'isChineseLocale', true);
|
||||
} else {
|
||||
vscode.commands.executeCommand('setContext', 'isChineseLocale', false);
|
||||
}
|
||||
regDevChatView(context);
|
||||
regTopicView(context);
|
||||
|
||||
registerApiKeySettingCommand(context);
|
||||
registerOpenChatPanelCommand(context);
|
||||
registerAddContextCommand(context);
|
||||
registerAskForCodeCommand(context);
|
||||
registerAskForFileCommand(context);
|
||||
registerStatusBarItemClickCommand(context);
|
||||
|
||||
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
||||
createStatusBarItem(context);
|
||||
|
||||
// Set the status bar item properties
|
||||
// const iconPath = context.asAbsolutePath(path.join('assets', 'tank.png'));
|
||||
|
||||
// Set the status bar item properties
|
||||
statusBarItem.text = `$(warning)DevChat`;
|
||||
statusBarItem.tooltip = 'DevChat checking ..., please wait.';
|
||||
statusBarItem.command = '';
|
||||
|
||||
// add a timer to update the status bar item
|
||||
let devchatStatus = '';
|
||||
let apiKeyStatus = '';
|
||||
let isVersionChangeCompare: boolean|undefined = undefined;
|
||||
setInterval(async () => {
|
||||
const versionOld = await secretStorage.get("DevChatVersionOld");
|
||||
const versionNew = extensionVersion;
|
||||
const versionChanged = versionOld !== versionNew;
|
||||
await secretStorage.store("DevChatVersionOld", versionNew!);
|
||||
|
||||
// status item has three status type
|
||||
// 1. not in a folder
|
||||
// 2. dependence is invalid
|
||||
// 3. ready
|
||||
if (devchatStatus === '' || devchatStatus === 'waiting install devchat') {
|
||||
let bOk = true;
|
||||
let devChat: string | undefined = vscode.workspace.getConfiguration('DevChat').get('DevChatPath');
|
||||
if (!devChat) {
|
||||
bOk = false;
|
||||
}
|
||||
|
||||
if (!bOk) {
|
||||
bOk = checkDevChatDependency();
|
||||
}
|
||||
if (bOk && versionChanged && !isVersionChangeCompare) {
|
||||
logger.channel()?.info(`versionOld: ${versionOld}, versionNew: ${versionNew}, versionChanged: ${versionChanged}`);
|
||||
bOk = false;
|
||||
}
|
||||
|
||||
if (bOk) {
|
||||
devchatStatus = 'ready';
|
||||
TopicManager.getInstance().loadTopics();
|
||||
} else {
|
||||
if (devchatStatus === '') {
|
||||
devchatStatus = 'not ready';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (devchatStatus === 'not ready') {
|
||||
// auto install devchat
|
||||
const terminal = vscode.window.createTerminal("DevChat Install");
|
||||
terminal.sendText(`python ${context.extensionUri.fsPath + "/tools/install.py"}`);
|
||||
terminal.show();
|
||||
devchatStatus = 'waiting install devchat';
|
||||
isVersionChangeCompare = true;
|
||||
}
|
||||
|
||||
if (devchatStatus !== 'ready') {
|
||||
statusBarItem.text = `$(warning)DevChat`;
|
||||
statusBarItem.tooltip = `${devchatStatus}`;
|
||||
statusBarItem.command = undefined;
|
||||
// set statusBarItem warning color
|
||||
return;
|
||||
}
|
||||
|
||||
// check api key
|
||||
if (apiKeyStatus === '' || apiKeyStatus === 'please set api key') {
|
||||
const bOk = await checkOpenaiApiKey();
|
||||
if (bOk) {
|
||||
apiKeyStatus = 'ready';
|
||||
} else {
|
||||
apiKeyStatus = 'please set api key';
|
||||
}
|
||||
}
|
||||
if (apiKeyStatus !== 'ready') {
|
||||
statusBarItem.text = `$(warning)DevChat`;
|
||||
statusBarItem.tooltip = `${apiKeyStatus}`;
|
||||
statusBarItem.command = 'DevChat.OPENAI_API_KEY';
|
||||
return;
|
||||
}
|
||||
|
||||
statusBarItem.text = `$(pass)DevChat`;
|
||||
statusBarItem.tooltip = `ready to chat`;
|
||||
statusBarItem.command = 'devcaht.onStatusBarClick';
|
||||
}, 3000);
|
||||
|
||||
// Add the status bar item to the status bar
|
||||
statusBarItem.show();
|
||||
context.subscriptions.push(statusBarItem);
|
||||
|
||||
// Register the command
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('devcaht.onStatusBarClick', async () => {
|
||||
await vscode.commands.executeCommand('devchat-view.focus');
|
||||
})
|
||||
);
|
||||
|
||||
ExtensionContextHolder.provider = new DevChatViewProvider(context);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider('devchat-view', ExtensionContextHolder.provider, {
|
||||
webviewOptions: { retainContextWhenHidden: true }
|
||||
})
|
||||
);
|
||||
|
||||
const yourTreeDataProvider = new TopicTreeDataProvider();
|
||||
const yourTreeView = vscode.window.createTreeView('devchat-topicview', {
|
||||
treeDataProvider: yourTreeDataProvider,
|
||||
});
|
||||
context.subscriptions.push(yourTreeView);
|
||||
|
||||
const topicDeleteCallback = async (item: TopicTreeItem) => {
|
||||
const confirm = 'Delete';
|
||||
const cancel = 'Cancel';
|
||||
const label = typeof item.label === 'string' ? item.label : item.label!.label;
|
||||
const truncatedLabel = label.substring(0, 20) + (label.length > 20 ? '...' : '');
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
`Are you sure you want to delete the topic "${truncatedLabel}"?`,
|
||||
{ modal: true },
|
||||
confirm,
|
||||
cancel
|
||||
);
|
||||
|
||||
if (result === confirm) {
|
||||
TopicManager.getInstance().deleteTopic(item.id);
|
||||
}
|
||||
};
|
||||
vscode.commands.registerCommand('devchat-topicview.deleteTopic', topicDeleteCallback);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeActionsProvider(
|
||||
{ pattern: '**', scheme: 'file' },
|
||||
{
|
||||
provideCodeActions: (document, range, context, token) => {
|
||||
const deleteAction = new vscode.CodeAction('Delete Item', vscode.CodeActionKind.QuickFix);
|
||||
deleteAction.command = {
|
||||
title: 'Delete Item',
|
||||
command: 'devchat-topicview.deleteTopic',
|
||||
arguments: [context.diagnostics[0].code],
|
||||
};
|
||||
return [deleteAction];
|
||||
},
|
||||
},
|
||||
{ providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }
|
||||
)
|
||||
);
|
||||
|
||||
vscode.commands.registerCommand('devchat-topicview.addTopic', () => {
|
||||
const topic = TopicManager.getInstance().createTopic();
|
||||
TopicManager.getInstance().setCurrentTopic(topic.topicId);
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('devchat-topicview.deleteSelectedTopic', () => {
|
||||
const selectedItem = yourTreeDataProvider.selectedItem;
|
||||
if (selectedItem) {
|
||||
topicDeleteCallback(selectedItem);
|
||||
} else {
|
||||
vscode.window.showErrorMessage('No item selected');
|
||||
}
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('devchat-topicview.selectTopic', (item: TopicTreeItem) => {
|
||||
yourTreeDataProvider.setSelectedItem(item);
|
||||
TopicManager.getInstance().setCurrentTopic(item.id);
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('devchat-topicview.reloadTopic', async (item: TopicTreeItem) => {
|
||||
TopicManager.getInstance().loadTopics();
|
||||
});
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('devchat.applyDiffResult', async (data) => {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
const fileName = activeEditor!.document.fileName;
|
||||
|
||||
const [leftUri, rightUri] = FilePairManager.getInstance().findPair(fileName) || [undefined, undefined];
|
||||
if (leftUri && rightUri) {
|
||||
// 获取对比的两个文件
|
||||
const leftDoc = await vscode.workspace.openTextDocument(leftUri);
|
||||
const rightDoc = await vscode.workspace.openTextDocument(rightUri);
|
||||
|
||||
// 将右边文档的内容替换到左边文档
|
||||
const leftEditor = await vscode.window.showTextDocument(leftDoc);
|
||||
await leftEditor.edit(editBuilder => {
|
||||
const fullRange = new vscode.Range(0, 0, leftDoc.lineCount, 0);
|
||||
editBuilder.replace(fullRange, rightDoc.getText());
|
||||
});
|
||||
|
||||
// 保存左边文档
|
||||
await leftDoc.save();
|
||||
} else {
|
||||
vscode.window.showErrorMessage('No file to apply diff result.');
|
||||
}
|
||||
})
|
||||
);
|
||||
regTopicDeleteCommand(context);
|
||||
regAddTopicCommand(context);
|
||||
regDeleteSelectTopicCommand(context);
|
||||
regSelectTopicCommand(context);
|
||||
regReloadTopicCommand(context);
|
||||
regApplyDiffResultCommand(context);
|
||||
}
|
||||
exports.activate = activate;
|
||||
|
@ -2,15 +2,19 @@ import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { MessageHandler } from './messageHandler';
|
||||
import { regInMessage, regOutMessage } from '../util/reg_messages';
|
||||
import { logger } from '../util/logger';
|
||||
|
||||
|
||||
regInMessage({command: 'contextDetail', file: ''});
|
||||
regOutMessage({command: 'contextDetailResponse', file: '', result: ''});
|
||||
regInMessage({ command: 'contextDetail', file: '' });
|
||||
regOutMessage({ command: 'contextDetailResponse', file: '', result: '' });
|
||||
// message: { command: 'contextDetail', file: string }
|
||||
// read detail context information from file
|
||||
// return json string
|
||||
export async function contextDetail(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
||||
const fileContent = fs.readFileSync(message.file, 'utf-8');
|
||||
MessageHandler.sendMessage(panel, { command: 'contextDetailResponse', 'file':message.file, result: fileContent });
|
||||
return;
|
||||
export async function contextDetail(message: any, panel: vscode.WebviewPanel | vscode.WebviewView): Promise<void> {
|
||||
try {
|
||||
const fileContent = fs.readFileSync(message.file, 'utf-8');
|
||||
MessageHandler.sendMessage(panel, { command: 'contextDetailResponse', 'file': message.file, result: fileContent });
|
||||
} catch (error) {
|
||||
logger.channel()?.error(`Error reading file ${ message.file }:, ${error}`);
|
||||
}
|
||||
}
|
||||
|
@ -1,141 +1,21 @@
|
||||
import * as vscode from 'vscode';
|
||||
import DevChat, { LogOptions, LogEntry } from '../toolwrapper/devchat';
|
||||
import { MessageHandler } from './messageHandler';
|
||||
import messageHistory from '../util/messageHistory';
|
||||
import { regInMessage, regOutMessage } from '../util/reg_messages';
|
||||
import { checkOpenaiApiKey } from '../contributes/commands';
|
||||
import ExtensionContextHolder from '../util/extensionContext';
|
||||
import { TopicManager } from '../topic/topicManager';
|
||||
import { historyMessagesBase, onApiKeyBase } from './historyMessagesBase';
|
||||
|
||||
|
||||
let isApiSet: boolean | undefined = undefined;
|
||||
|
||||
interface LoadHistoryMessages {
|
||||
command: string;
|
||||
entries: Array<LogEntry>;
|
||||
}
|
||||
|
||||
function welcomeMessage(): LogEntry {
|
||||
// create default logEntry to show welcome message
|
||||
return {
|
||||
hash: 'message',
|
||||
parent: '',
|
||||
user: 'system',
|
||||
date: '',
|
||||
request: 'How do I use DevChat?',
|
||||
response: `
|
||||
Do you want to write some code or have a question about the project? Simply right-click on your chosen files or code snippets and add them to DevChat. Feel free to ask me anything or let me help you with coding.
|
||||
|
||||
Don't forget to check out the "+" button on the left of the input to add more context. To see a list of workflows you can run in the context, just type "/". Happy prompting!
|
||||
`,
|
||||
context: []
|
||||
} as LogEntry;
|
||||
}
|
||||
|
||||
function apiKeyMissedMessage(): LogEntry {
|
||||
// create default logEntry to show welcome message
|
||||
return {
|
||||
hash: 'message',
|
||||
parent: '',
|
||||
user: 'system',
|
||||
date: '',
|
||||
request: 'Is OPENAI_API_KEY ready?',
|
||||
response: `
|
||||
OPENAI_API_KEY is missing from your environment or settings. Kindly input your OpenAI or DevChat key, and I'll ensure DevChat is all set for you.
|
||||
`,
|
||||
context: []
|
||||
} as LogEntry;
|
||||
}
|
||||
|
||||
|
||||
regInMessage({command: 'historyMessages', options: { skip: 0, maxCount: 0 }});
|
||||
regOutMessage({command: 'loadHistoryMessages', entries: [{hash: '',user: '',date: '',request: '',response: '',context: [{content: '',role: ''}]}]});
|
||||
export async function historyMessages(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
||||
const topicId = TopicManager.getInstance().currentTopicId;
|
||||
let logEntriesFlat: Array<LogEntry> = [];
|
||||
if (topicId) {
|
||||
logEntriesFlat = await TopicManager.getInstance().getTopicHistory(topicId);
|
||||
const historyMessage = await historyMessagesBase();
|
||||
if (historyMessage) {
|
||||
MessageHandler.sendMessage(panel, historyMessage);
|
||||
}
|
||||
messageHistory.clear();
|
||||
|
||||
// TODO handle context
|
||||
const logEntriesFlatFiltered = logEntriesFlat.map((entry) => {
|
||||
return {
|
||||
date: entry.date,
|
||||
hash: entry.hash,
|
||||
request: entry.request,
|
||||
text: entry.response,
|
||||
user: entry.user,
|
||||
parentHash: '',
|
||||
};
|
||||
});
|
||||
|
||||
for (let i = 0; i < logEntriesFlat.length; i++) {
|
||||
let entryOld = logEntriesFlat[i];
|
||||
let entryNew = {
|
||||
date: entryOld.date,
|
||||
hash: entryOld.hash,
|
||||
request: entryOld.request,
|
||||
text: entryOld.response,
|
||||
user: entryOld.user,
|
||||
parentHash: '',
|
||||
};
|
||||
if (i > 0) {
|
||||
entryNew.parentHash = logEntriesFlat[i - 1].hash;
|
||||
}
|
||||
messageHistory.add(entryNew);
|
||||
}
|
||||
|
||||
const isApiKeyReady = await checkOpenaiApiKey();
|
||||
isApiSet = true;
|
||||
if (!isApiKeyReady) {
|
||||
const startMessage = [ apiKeyMissedMessage() ];
|
||||
isApiSet = false;
|
||||
|
||||
MessageHandler.sendMessage(panel, {
|
||||
command: 'loadHistoryMessages',
|
||||
entries: startMessage,
|
||||
} as LoadHistoryMessages);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadHistoryMessages: LoadHistoryMessages = {
|
||||
command: 'loadHistoryMessages',
|
||||
entries: logEntriesFlat.length>0? logEntriesFlat : [welcomeMessage()],
|
||||
};
|
||||
|
||||
MessageHandler.sendMessage(panel, loadHistoryMessages);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
export function isValidApiKey(apiKey: string) {
|
||||
let apiKeyStrim = apiKey.trim();
|
||||
if (apiKeyStrim.indexOf('sk-') !== 0 && apiKeyStrim.indexOf('DC.') !== 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function isWaitForApiKey() {
|
||||
if (isApiSet === undefined) {
|
||||
isApiSet = await checkOpenaiApiKey();
|
||||
}
|
||||
return !isApiSet;
|
||||
}
|
||||
|
||||
export async function onApiKey(apiKey: string, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
||||
if (!isValidApiKey(apiKey)) {
|
||||
MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: 'Your API key is invalid. We support OpenAI and DevChat keys. Please reset the key.', hash: '', user: 'system', date: '', isError: false });
|
||||
return;
|
||||
}
|
||||
|
||||
isApiSet = true;
|
||||
|
||||
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context?.secrets!;
|
||||
secretStorage.store("devchat_OPENAI_API_KEY", apiKey);
|
||||
|
||||
const welcomeMessageText = welcomeMessage().response;
|
||||
MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: `Your OPENAI_API_KEY is set. Enjoy DevChat!\n${welcomeMessageText}`, hash: '', user: 'system', date: '', isError: false });
|
||||
const resMessage = await onApiKeyBase(apiKey);
|
||||
MessageHandler.sendMessage(panel, resMessage);
|
||||
}
|
||||
|
||||
|
143
src/handler/historyMessagesBase.ts
Normal file
143
src/handler/historyMessagesBase.ts
Normal file
@ -0,0 +1,143 @@
|
||||
|
||||
|
||||
import { TopicManager } from '../topic/topicManager';
|
||||
import { LogEntry } from '../toolwrapper/devchat';
|
||||
import messageHistory from '../util/messageHistory';
|
||||
import { ApiKeyManager } from '../util/apiKey';
|
||||
import { logger } from '../util/logger';
|
||||
|
||||
let isApiSet: boolean | undefined = undefined;
|
||||
|
||||
interface LoadHistoryMessages {
|
||||
command: string;
|
||||
entries: Array<LogEntry>;
|
||||
}
|
||||
|
||||
function welcomeMessage(): LogEntry {
|
||||
// create default logEntry to show welcome message
|
||||
return {
|
||||
hash: 'message',
|
||||
parent: '',
|
||||
user: 'system',
|
||||
date: '',
|
||||
request: 'How do I use DevChat?',
|
||||
response: `
|
||||
Do you want to write some code or have a question about the project? Simply right-click on your chosen files or code snippets and add them to DevChat. Feel free to ask me anything or let me help you with coding.
|
||||
|
||||
Don't forget to check out the "+" button on the left of the input to add more context. To see a list of workflows you can run in the context, just type "/". Happy prompting!
|
||||
`,
|
||||
context: []
|
||||
} as LogEntry;
|
||||
}
|
||||
|
||||
function apiKeyMissedMessage(): LogEntry {
|
||||
// create default logEntry to show welcome message
|
||||
return {
|
||||
hash: 'message',
|
||||
parent: '',
|
||||
user: 'system',
|
||||
date: '',
|
||||
request: 'Is OPENAI_API_KEY ready?',
|
||||
response: `
|
||||
OPENAI_API_KEY is missing from your environment or settings. Kindly input your OpenAI or DevChat key, and I'll ensure DevChat is all set for you.
|
||||
`,
|
||||
context: []
|
||||
} as LogEntry;
|
||||
}
|
||||
|
||||
export function isValidApiKey(apiKey: string) {
|
||||
let apiKeyStrim = apiKey.trim();
|
||||
if (ApiKeyManager.getKeyType(apiKeyStrim) === undefined) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function isWaitForApiKey() {
|
||||
if (isApiSet === undefined) {
|
||||
const apiKey = await ApiKeyManager.getApiKey();
|
||||
isApiSet = apiKey !== undefined;
|
||||
}
|
||||
return !isApiSet;
|
||||
}
|
||||
|
||||
export async function loadTopicHistoryLogs() : Promise<Array<LogEntry> | undefined> {
|
||||
const topicId = TopicManager.getInstance().currentTopicId;
|
||||
let logEntriesFlat: Array<LogEntry> = [];
|
||||
if (topicId) {
|
||||
logEntriesFlat = await TopicManager.getInstance().getTopicHistory(topicId);
|
||||
}
|
||||
|
||||
if (topicId !== TopicManager.getInstance().currentTopicId) {
|
||||
logger.channel()?.info(`Current topic changed dure load topic hsitory!`)
|
||||
return undefined;
|
||||
}
|
||||
return logEntriesFlat;
|
||||
}
|
||||
|
||||
export function updateCurrentMessageHistory(logEntries: Array<LogEntry>): void {
|
||||
messageHistory.clear();
|
||||
|
||||
for (let i = 0; i < logEntries.length; i++) {
|
||||
let entryOld = logEntries[i];
|
||||
let entryNew = {
|
||||
date: entryOld.date,
|
||||
hash: entryOld.hash,
|
||||
request: entryOld.request,
|
||||
text: entryOld.response,
|
||||
user: entryOld.user,
|
||||
parentHash: '',
|
||||
};
|
||||
if (i > 0) {
|
||||
entryNew.parentHash = logEntries[i - 1].hash;
|
||||
}
|
||||
messageHistory.add(entryNew);
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiKeyInvalidMessage(): Promise<LoadHistoryMessages|undefined> {
|
||||
const apiKey = await ApiKeyManager.getApiKey();
|
||||
isApiSet = true;
|
||||
if (!apiKey) {
|
||||
const startMessage = [ apiKeyMissedMessage() ];
|
||||
isApiSet = false;
|
||||
|
||||
return {
|
||||
command: 'loadHistoryMessages',
|
||||
entries: startMessage,
|
||||
} as LoadHistoryMessages;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function historyMessagesBase(): Promise<LoadHistoryMessages | undefined> {
|
||||
const logEntriesFlat = await loadTopicHistoryLogs();
|
||||
if (!logEntriesFlat) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
updateCurrentMessageHistory(logEntriesFlat);
|
||||
|
||||
const apiKeyMessage = await apiKeyInvalidMessage();
|
||||
if (apiKeyMessage !== undefined) {
|
||||
return apiKeyMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
command: 'loadHistoryMessages',
|
||||
entries: logEntriesFlat.length>0? logEntriesFlat : [welcomeMessage()],
|
||||
} as LoadHistoryMessages;
|
||||
}
|
||||
|
||||
export async function onApiKeyBase(apiKey: string): Promise<{command: string, text: string, hash: string, user: string, date: string, isError: boolean}> {
|
||||
if (!isValidApiKey(apiKey)) {
|
||||
return { command: 'receiveMessage', text: 'Your API key is invalid. We support OpenAI and DevChat keys. Please reset the key.', hash: '', user: 'system', date: '', isError: false };
|
||||
}
|
||||
|
||||
isApiSet = true;
|
||||
ApiKeyManager.writeApiKeySecret(apiKey);
|
||||
|
||||
const welcomeMessageText = welcomeMessage().response;
|
||||
return { command: 'receiveMessage', text: `Your OPENAI_API_KEY is set. Enjoy DevChat!\n${welcomeMessageText}`, hash: '', user: 'system', date: '', isError: false };
|
||||
}
|
@ -5,9 +5,9 @@ import * as vscode from 'vscode';
|
||||
import '../command/loadCommands';
|
||||
import '../context/loadContexts';
|
||||
import { logger } from '../util/logger';
|
||||
import { on } from 'events';
|
||||
import { isWaitForApiKey, onApiKey } from './historyMessages';
|
||||
import { checkOpenaiApiKey } from '../contributes/commands';
|
||||
import { isWaitForApiKey } from './historyMessagesBase';
|
||||
import { onApiKey } from './historyMessages';
|
||||
import { ApiKeyManager } from '../util/apiKey';
|
||||
|
||||
|
||||
export class MessageHandler {
|
||||
@ -20,6 +20,8 @@ export class MessageHandler {
|
||||
this.handlers[command] = handler;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async handleMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
||||
let isNeedSendResponse = false;
|
||||
if (message.command === 'sendMessage') {
|
||||
@ -34,7 +36,7 @@ export class MessageHandler {
|
||||
}
|
||||
}
|
||||
if (message.command === 'sendMessage') {
|
||||
if (await isWaitForApiKey() && !await checkOpenaiApiKey()) {
|
||||
if (await isWaitForApiKey() && !await ApiKeyManager.getApiKey()) {
|
||||
onApiKey(message.text, panel);
|
||||
return;
|
||||
}
|
||||
|
@ -1,76 +1,12 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import DevChat, { ChatResponse } from '../toolwrapper/devchat';
|
||||
import CommandManager from '../command/commandManager';
|
||||
import { logger } from '../util/logger';
|
||||
import { MessageHandler } from './messageHandler';
|
||||
import messageHistory from '../util/messageHistory';
|
||||
import CustomCommands from '../command/customCommand';
|
||||
import { regInMessage, regOutMessage } from '../util/reg_messages';
|
||||
import { TopicManager } from '../topic/topicManager';
|
||||
import { stopDevChatBase, sendMessageBase } from './sendMessageBase';
|
||||
|
||||
|
||||
let _lastMessage: any = undefined;
|
||||
|
||||
|
||||
// Add this function to messageHandler.ts
|
||||
function parseMessage(message: string): { context: string[]; instruction: string[]; reference: string[]; text: string } {
|
||||
const contextRegex = /\[context\|(.*?)\]/g;
|
||||
const instructionRegex = /\[instruction\|(.*?)\]/g;
|
||||
const referenceRegex = /\[reference\|(.*?)\]/g;
|
||||
|
||||
const contextPaths = [];
|
||||
const instructionPaths = [];
|
||||
const referencePaths = [];
|
||||
|
||||
let match;
|
||||
|
||||
// 提取 context
|
||||
while ((match = contextRegex.exec(message)) !== null) {
|
||||
contextPaths.push(match[1]);
|
||||
}
|
||||
|
||||
// 提取 instruction
|
||||
while ((match = instructionRegex.exec(message)) !== null) {
|
||||
instructionPaths.push(match[1]);
|
||||
}
|
||||
|
||||
// 提取 reference
|
||||
while ((match = referenceRegex.exec(message)) !== null) {
|
||||
referencePaths.push(match[1]);
|
||||
}
|
||||
|
||||
// 移除标签,保留纯文本
|
||||
const text = message
|
||||
.replace(contextRegex, '')
|
||||
.replace(instructionRegex, '')
|
||||
.replace(referenceRegex, '')
|
||||
.trim();
|
||||
|
||||
return { context: contextPaths, instruction: instructionPaths, reference: referencePaths, text };
|
||||
}
|
||||
|
||||
function getInstructionFiles(): string[] {
|
||||
const instructionFiles: string[] = [];
|
||||
|
||||
const customCommands = CustomCommands.getInstance().getCommands();
|
||||
// visit customCommands, get default command
|
||||
for (const command of customCommands) {
|
||||
if (command.default) {
|
||||
for (const instruction of command.instructions) {
|
||||
instructionFiles.push(`./.chat/workflows/${command.name}/${instruction}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instructionFiles;
|
||||
}
|
||||
|
||||
const devChat = new DevChat();
|
||||
let userStop = false;
|
||||
|
||||
|
||||
regInMessage({command: 'sendMessage', text: '', hash: undefined});
|
||||
regOutMessage({ command: 'receiveMessage', text: 'xxxx', hash: 'xxx', user: 'xxx', date: 'xxx'});
|
||||
regOutMessage({ command: 'receiveMessagePartial', text: 'xxxx', user: 'xxx', date: 'xxx'});
|
||||
@ -81,72 +17,12 @@ regOutMessage({ command: 'receiveMessagePartial', text: 'xxxx', user: 'xxx', dat
|
||||
export async function sendMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
||||
_lastMessage = message;
|
||||
|
||||
const newText2 = await CommandManager.getInstance().processText(message.text);
|
||||
const parsedMessage = parseMessage(newText2);
|
||||
const chatOptions: any = {};
|
||||
|
||||
let parentHash = undefined;
|
||||
logger.channel()?.info(`request message hash: ${message.hash}`);
|
||||
if (message.hash) {
|
||||
const hmessage = messageHistory.find(message.hash);
|
||||
parentHash = hmessage ? message.parentHash : undefined;
|
||||
} else {
|
||||
const hmessage = messageHistory.findLast();
|
||||
parentHash = hmessage ? hmessage.hash : undefined;
|
||||
const responseMessage = await sendMessageBase(message, (data: { command: string, text: string, user: string, date: string}) => {
|
||||
MessageHandler.sendMessage(panel, data, false);
|
||||
});
|
||||
if (responseMessage) {
|
||||
MessageHandler.sendMessage(panel, responseMessage);
|
||||
}
|
||||
if (parentHash) {
|
||||
chatOptions.parent = parentHash;
|
||||
}
|
||||
logger.channel()?.info(`parent hash: ${parentHash}`);
|
||||
|
||||
if (parsedMessage.context.length > 0) {
|
||||
chatOptions.context = parsedMessage.context;
|
||||
}
|
||||
|
||||
chatOptions.header = getInstructionFiles();
|
||||
if (parsedMessage.instruction.length > 0) {
|
||||
chatOptions.header = parsedMessage.instruction;
|
||||
}
|
||||
|
||||
if (parsedMessage.reference.length > 0) {
|
||||
chatOptions.reference = parsedMessage.reference;
|
||||
}
|
||||
|
||||
let partialDataText = '';
|
||||
const onData = (partialResponse: ChatResponse) => {
|
||||
const responseText = partialResponse.response.replace(/```\ncommitmsg/g, "```commitmsg");
|
||||
partialDataText = responseText;
|
||||
MessageHandler.sendMessage(panel, { command: 'receiveMessagePartial', text: responseText, user: partialResponse.user, date: partialResponse.date }, false);
|
||||
};
|
||||
|
||||
const chatResponse = await devChat.chat(parsedMessage.text, chatOptions, onData);
|
||||
|
||||
if (!chatResponse.isError) {
|
||||
messageHistory.add({request: message.text, text: chatResponse.response, parentHash, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date });
|
||||
|
||||
let topicId = TopicManager.getInstance().currentTopicId;
|
||||
if (!topicId) {
|
||||
// create new topic
|
||||
const topic = TopicManager.getInstance().createTopic();
|
||||
topicId = topic.topicId;
|
||||
}
|
||||
|
||||
TopicManager.getInstance().updateTopic(topicId!, chatResponse['prompt-hash'], Number(chatResponse.date), message.text, chatResponse.response);
|
||||
}
|
||||
|
||||
let responseText = chatResponse.response.replace(/```\ncommitmsg/g, "```commitmsg");
|
||||
if (userStop) {
|
||||
userStop = false;
|
||||
if (responseText.indexOf('Exit code: undefined') >= 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (chatResponse.isError) {
|
||||
responseText = partialDataText + responseText;
|
||||
}
|
||||
|
||||
MessageHandler.sendMessage(panel, { command: 'receiveMessage', text: responseText, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date, isError: chatResponse.isError });
|
||||
return;
|
||||
}
|
||||
|
||||
// regeneration last message again
|
||||
@ -160,9 +36,7 @@ export async function regeneration(message: any, panel: vscode.WebviewPanel|vsco
|
||||
|
||||
regInMessage({command: 'stopDevChat'});
|
||||
export async function stopDevChat(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
||||
logger.channel()?.info(`Stopping devchat`);
|
||||
userStop = true;
|
||||
devChat.stop();
|
||||
stopDevChatBase(message);
|
||||
}
|
||||
|
||||
|
||||
|
160
src/handler/sendMessageBase.ts
Normal file
160
src/handler/sendMessageBase.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import DevChat, { ChatResponse } from '../toolwrapper/devchat';
|
||||
import CommandManager from '../command/commandManager';
|
||||
import { logger } from '../util/logger';
|
||||
import messageHistory from '../util/messageHistory';
|
||||
import { TopicManager } from '../topic/topicManager';
|
||||
import CustomCommands from '../command/customCommand';
|
||||
|
||||
|
||||
// Add this function to messageHandler.ts
|
||||
export function parseMessage(message: string): { context: string[]; instruction: string[]; reference: string[]; text: string } {
|
||||
const contextRegex = /\[context\|(.*?)\]/g;
|
||||
const instructionRegex = /\[instruction\|(.*?)\]/g;
|
||||
const referenceRegex = /\[reference\|(.*?)\]/g;
|
||||
|
||||
const contextPaths = [];
|
||||
const instructionPaths = [];
|
||||
const referencePaths = [];
|
||||
|
||||
let match;
|
||||
|
||||
// 提取 context
|
||||
while ((match = contextRegex.exec(message)) !== null) {
|
||||
contextPaths.push(match[1]);
|
||||
}
|
||||
|
||||
// 提取 instruction
|
||||
while ((match = instructionRegex.exec(message)) !== null) {
|
||||
instructionPaths.push(match[1]);
|
||||
}
|
||||
|
||||
// 提取 reference
|
||||
while ((match = referenceRegex.exec(message)) !== null) {
|
||||
referencePaths.push(match[1]);
|
||||
}
|
||||
|
||||
// 移除标签,保留纯文本
|
||||
const text = message
|
||||
.replace(contextRegex, '')
|
||||
.replace(instructionRegex, '')
|
||||
.replace(referenceRegex, '')
|
||||
.trim();
|
||||
|
||||
return { context: contextPaths, instruction: instructionPaths, reference: referencePaths, text };
|
||||
}
|
||||
|
||||
export function getInstructionFiles(): string[] {
|
||||
const instructionFiles: string[] = [];
|
||||
|
||||
const customCommands = CustomCommands.getInstance().getCommands();
|
||||
// visit customCommands, get default command
|
||||
for (const command of customCommands) {
|
||||
if (command.default) {
|
||||
for (const instruction of command.instructions) {
|
||||
instructionFiles.push(`./.chat/workflows/${command.name}/${instruction}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instructionFiles;
|
||||
}
|
||||
|
||||
const devChat = new DevChat();
|
||||
let userStop = false;
|
||||
|
||||
|
||||
// 将解析消息的部分提取到一个单独的函数中
|
||||
export async function parseMessageAndSetOptions(message: any, chatOptions: any): Promise<{ context: string[]; instruction: string[]; reference: string[]; text: string }> {
|
||||
const newText2 = await CommandManager.getInstance().processText(message.text);
|
||||
const parsedMessage = parseMessage(newText2);
|
||||
|
||||
if (parsedMessage.context.length > 0) {
|
||||
chatOptions.context = parsedMessage.context;
|
||||
}
|
||||
|
||||
chatOptions.header = getInstructionFiles();
|
||||
if (parsedMessage.instruction.length > 0) {
|
||||
chatOptions.header = parsedMessage.instruction;
|
||||
}
|
||||
|
||||
if (parsedMessage.reference.length > 0) {
|
||||
chatOptions.reference = parsedMessage.reference;
|
||||
}
|
||||
return parsedMessage;
|
||||
}
|
||||
|
||||
// 将处理父哈希的部分提取到一个单独的函数中
|
||||
export function getParentHash(message: any): string|undefined {
|
||||
let parentHash = undefined;
|
||||
logger.channel()?.info(`request message hash: ${message.hash}`);
|
||||
if (message.hash) {
|
||||
const hmessage = messageHistory.find(message.hash);
|
||||
parentHash = hmessage ? hmessage.parentHash : undefined;
|
||||
} else {
|
||||
const hmessage = messageHistory.findLast();
|
||||
parentHash = hmessage ? hmessage.hash : undefined;
|
||||
}
|
||||
logger.channel()?.info(`parent hash: ${parentHash}`);
|
||||
return parentHash;
|
||||
}
|
||||
|
||||
export async function handleTopic(parentHash:string, message: any, chatResponse: ChatResponse) {
|
||||
if (!chatResponse.isError) {
|
||||
messageHistory.add({ request: message.text, text: chatResponse.response, parentHash, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date });
|
||||
|
||||
let topicId = TopicManager.getInstance().currentTopicId;
|
||||
if (!topicId) {
|
||||
// create new topic
|
||||
const topic = TopicManager.getInstance().createTopic();
|
||||
topicId = topic.topicId;
|
||||
}
|
||||
|
||||
TopicManager.getInstance().updateTopic(topicId!, chatResponse['prompt-hash'], Number(chatResponse.date), message.text, chatResponse.response);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handlerResponseText(partialDataText: string, chatResponse: ChatResponse) : Promise<string|undefined> {
|
||||
let responseText = chatResponse.response.replace(/```\ncommitmsg/g, "```commitmsg");
|
||||
if (userStop) {
|
||||
userStop = false;
|
||||
if (responseText == '' && chatResponse.isError) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (chatResponse.isError) {
|
||||
responseText = partialDataText + '\n\n' + responseText;
|
||||
}
|
||||
return responseText;
|
||||
}
|
||||
|
||||
// 重构后的sendMessage函数
|
||||
export async function sendMessageBase(message: any, handlePartialData: (data: { command: string, text: string, user: string, date: string}) => void): Promise<{ command: string, text: string, hash: string, user: string, date: string, isError: boolean }|undefined> {
|
||||
const chatOptions: any = {};
|
||||
const parsedMessage = await parseMessageAndSetOptions(message, chatOptions);
|
||||
|
||||
const parentHash = getParentHash(message);
|
||||
if (parentHash) {
|
||||
chatOptions.parent = parentHash;
|
||||
}
|
||||
|
||||
let partialDataText = '';
|
||||
const onData = (partialResponse: ChatResponse) => {
|
||||
partialDataText = partialResponse.response.replace(/```\ncommitmsg/g, "```commitmsg");
|
||||
handlePartialData({ command: 'receiveMessagePartial', text: partialDataText!, user: partialResponse.user, date: partialResponse.date });
|
||||
};
|
||||
|
||||
const chatResponse = await devChat.chat(parsedMessage.text, chatOptions, onData);
|
||||
await handleTopic(parentHash!, message, chatResponse);
|
||||
const responseText = await handlerResponseText(partialDataText, chatResponse);
|
||||
if (responseText === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
return { command: 'receiveMessage', text: responseText, hash: chatResponse['prompt-hash'], user: chatResponse.user, date: chatResponse.date, isError: chatResponse.isError };
|
||||
}
|
||||
|
||||
export async function stopDevChatBase(message: any): Promise<void> {
|
||||
logger.channel()?.info(`Stopping devchat`);
|
||||
userStop = true;
|
||||
devChat.stop();
|
||||
}
|
@ -8,6 +8,7 @@ import { regInMessage, regOutMessage } from '../util/reg_messages';
|
||||
import { applyCodeChanges, isValidActionString } from '../util/appyDiff';
|
||||
import { logger } from '../util/logger';
|
||||
import { FilePairManager } from '../util/diffFilePairs';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
|
||||
|
||||
@ -64,7 +65,7 @@ export async function diffView(code: string, tarFile: string) {
|
||||
const tempFile = path.join(tempDir, fileName);
|
||||
|
||||
// save code to temp file
|
||||
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFile), Buffer.from(code));
|
||||
await UiUtilWrapper.writeFile(tempFile, code);
|
||||
|
||||
// open diff view
|
||||
FilePairManager.getInstance().addFilePair(curFile, tempFile);
|
||||
|
@ -1,9 +1,9 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as ncp from 'ncp';
|
||||
|
||||
import { logger } from '../util/logger';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
|
||||
function copyFileSync(source: string, target: string) {
|
||||
@ -32,13 +32,11 @@ function copyFileSync(source: string, target: string) {
|
||||
}
|
||||
|
||||
export function createChatDirectoryAndCopyInstructionsSync(extensionUri: vscode.Uri) {
|
||||
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders) {
|
||||
const workspaceRoot = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
if (!workspaceRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceRoot = workspaceFolders[0].uri.fsPath;
|
||||
const chatWorkflowsDirPath = path.join(workspaceRoot, '.chat', 'workflows');
|
||||
const instructionsSrcPath = path.join(extensionUri.fsPath, 'workflows');
|
||||
|
||||
|
@ -9,6 +9,7 @@ import WebviewManager from './webviewManager';
|
||||
import CustomCommands from '../command/customCommand';
|
||||
import CommandManager from '../command/commandManager';
|
||||
import { createChatDirectoryAndCopyInstructionsSync } from '../init/chatConfig';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
export default class ChatPanel {
|
||||
private static _instance: ChatPanel | undefined;
|
||||
@ -20,7 +21,7 @@ export default class ChatPanel {
|
||||
// 创建 .chat 目录并复制 workflows
|
||||
createChatDirectoryAndCopyInstructionsSync(extensionUri);
|
||||
|
||||
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
|
||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
if (workspaceDir) {
|
||||
const workflowsDir = path.join(workspaceDir!, '.chat', 'workflows');
|
||||
CustomCommands.getInstance().parseCommands(workflowsDir);
|
||||
|
@ -8,6 +8,7 @@ import { createChatDirectoryAndCopyInstructionsSync } from '../init/chatConfig';
|
||||
import ExtensionContextHolder from '../util/extensionContext';
|
||||
import CustomCommands from '../command/customCommand';
|
||||
import { TopicManager } from '../topic/topicManager';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
|
||||
export class DevChatViewProvider implements vscode.WebviewViewProvider {
|
||||
@ -27,7 +28,7 @@ export class DevChatViewProvider implements vscode.WebviewViewProvider {
|
||||
// 创建 .chat 目录并复制 workflows
|
||||
createChatDirectoryAndCopyInstructionsSync(ExtensionContextHolder.context?.extensionUri!);
|
||||
|
||||
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
|
||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
if (workspaceDir) {
|
||||
const workflowsDir = path.join(workspaceDir!, '.chat', 'workflows');
|
||||
CustomCommands.getInstance().parseCommands(workflowsDir);
|
||||
|
42
src/panel/statusBarView.ts
Normal file
42
src/panel/statusBarView.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { dependencyCheck } from './statusBarViewBase';
|
||||
|
||||
|
||||
export function createStatusBarItem(context: vscode.ExtensionContext): vscode.StatusBarItem {
|
||||
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
||||
|
||||
// Set the status bar item properties
|
||||
statusBarItem.text = `$(warning)DevChat`;
|
||||
statusBarItem.tooltip = 'DevChat checking ..., please wait.';
|
||||
statusBarItem.command = '';
|
||||
|
||||
// add a timer to update the status bar item
|
||||
setInterval(async () => {
|
||||
const [devchatStatus, apiKeyStatus] = await dependencyCheck();
|
||||
if (devchatStatus !== 'ready') {
|
||||
statusBarItem.text = `$(warning)DevChat`;
|
||||
statusBarItem.tooltip = `${devchatStatus}`;
|
||||
statusBarItem.command = undefined;
|
||||
// set statusBarItem warning color
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiKeyStatus !== 'ready') {
|
||||
statusBarItem.text = `$(warning)DevChat`;
|
||||
statusBarItem.tooltip = `${apiKeyStatus}`;
|
||||
statusBarItem.command = 'DevChat.OPENAI_API_KEY';
|
||||
return;
|
||||
}
|
||||
|
||||
statusBarItem.text = `$(pass)DevChat`;
|
||||
statusBarItem.tooltip = `ready to chat`;
|
||||
statusBarItem.command = 'devcaht.onStatusBarClick';
|
||||
}, 3000);
|
||||
|
||||
// Add the status bar item to the status bar
|
||||
statusBarItem.show();
|
||||
|
||||
context.subscriptions.push(statusBarItem);
|
||||
return statusBarItem;
|
||||
}
|
81
src/panel/statusBarViewBase.ts
Normal file
81
src/panel/statusBarViewBase.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { logger } from "../util/logger";
|
||||
|
||||
import { UiUtilWrapper } from "../util/uiUtil";
|
||||
import { TopicManager } from "../topic/topicManager";
|
||||
import { checkDevChatDependency } from "../contributes/commandsBase";
|
||||
import { ApiKeyManager } from '../util/apiKey';
|
||||
|
||||
|
||||
|
||||
function getExtensionVersion(): string {
|
||||
const packageJsonPath = path.join(UiUtilWrapper.extensionPath(), 'package.json');
|
||||
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
|
||||
const packageJson = JSON.parse(packageJsonContent);
|
||||
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
let devchatStatus = '';
|
||||
let apiKeyStatus = '';
|
||||
let isVersionChangeCompare: boolean|undefined = undefined;
|
||||
export async function dependencyCheck(): Promise<[string, string]> {
|
||||
let versionChanged = false;
|
||||
if (isVersionChangeCompare === undefined) {
|
||||
const versionOld = await UiUtilWrapper.secretStorageGet("DevChatVersionOld");
|
||||
const versionNew = getExtensionVersion();
|
||||
const versionChanged = versionOld !== versionNew;
|
||||
UiUtilWrapper.storeSecret("DevChatVersionOld", versionNew!);
|
||||
|
||||
isVersionChangeCompare = true;
|
||||
logger.channel()?.info(`versionOld: ${versionOld}, versionNew: ${versionNew}, versionChanged: ${versionChanged}`);
|
||||
}
|
||||
|
||||
|
||||
// status item has three status type
|
||||
// 1. not in a folder
|
||||
// 2. dependence is invalid
|
||||
// 3. ready
|
||||
if (devchatStatus === '' || devchatStatus === 'waiting install devchat') {
|
||||
let bOk = true;
|
||||
let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath');
|
||||
if (!devChat) {
|
||||
bOk = false;
|
||||
}
|
||||
|
||||
if (!bOk) {
|
||||
bOk = checkDevChatDependency();
|
||||
}
|
||||
if (bOk && versionChanged) {
|
||||
bOk = false;
|
||||
}
|
||||
|
||||
if (bOk) {
|
||||
devchatStatus = 'ready';
|
||||
TopicManager.getInstance().loadTopics();
|
||||
} else {
|
||||
if (devchatStatus === '') {
|
||||
devchatStatus = 'not ready';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (devchatStatus === 'not ready') {
|
||||
// auto install devchat
|
||||
UiUtilWrapper.runTerminal('DevChat Install', `python ${UiUtilWrapper.extensionPath() + "/tools/install.py"}`);
|
||||
devchatStatus = 'waiting install devchat';
|
||||
isVersionChangeCompare = true;
|
||||
}
|
||||
|
||||
// check api key
|
||||
if (apiKeyStatus === '' || apiKeyStatus === 'please set api key') {
|
||||
const bOk = await ApiKeyManager.getApiKey();
|
||||
if (bOk) {
|
||||
apiKeyStatus = 'ready';
|
||||
} else {
|
||||
apiKeyStatus = 'please set api key';
|
||||
}
|
||||
}
|
||||
|
||||
return [devchatStatus, apiKeyStatus];
|
||||
}
|
129
src/panel/topicView.ts
Normal file
129
src/panel/topicView.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { TopicManager, Topic } from '../topic/topicManager';
|
||||
|
||||
|
||||
export class TopicTreeItem extends vscode.TreeItem {
|
||||
id: string;
|
||||
date: number | undefined;
|
||||
constructor(label: string, id: string, date: number | undefined, collapsibleState: vscode.TreeItemCollapsibleState) {
|
||||
super(label, collapsibleState);
|
||||
this.id = id;
|
||||
this.date = date;
|
||||
this.iconPath = new vscode.ThemeIcon('symbol-variable');
|
||||
this.contextValue = 'yourTreeItem';
|
||||
}
|
||||
uncheck() {
|
||||
this.iconPath = new vscode.ThemeIcon('symbol-variable');
|
||||
}
|
||||
check() {
|
||||
this.iconPath = new vscode.ThemeIcon('check');
|
||||
}
|
||||
}
|
||||
|
||||
export class TopicTreeDataProvider implements vscode.TreeDataProvider<TopicTreeItem> {
|
||||
private static instance: TopicTreeDataProvider;
|
||||
|
||||
public static getInstance(): TopicTreeDataProvider {
|
||||
if (!TopicTreeDataProvider.instance) {
|
||||
TopicTreeDataProvider.instance = new TopicTreeDataProvider();
|
||||
}
|
||||
return TopicTreeDataProvider.instance;
|
||||
}
|
||||
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<TopicTreeItem | undefined | null | void> = new vscode.EventEmitter<TopicTreeItem | undefined | null | void>();
|
||||
readonly onDidChangeTreeData: vscode.Event<TopicTreeItem | undefined | null | void> = this._onDidChangeTreeData.event;
|
||||
|
||||
public selectedItem: TopicTreeItem | null = null;
|
||||
private items: TopicTreeItem[] = [];
|
||||
|
||||
// reg listeners to TopicManager in constructor
|
||||
private constructor() {
|
||||
TopicManager.getInstance().addOnCreateTopicListener(this.addItem.bind(this));
|
||||
TopicManager.getInstance().addOnDeleteTopicListener(this.onDeleteTopic.bind(this));
|
||||
TopicManager.getInstance().addOnReloadTopicsListener(this.onReloadTopics.bind(this));
|
||||
TopicManager.getInstance().addOnUpdateTopicListener(this.onUpdateTopics.bind(this));
|
||||
}
|
||||
|
||||
// sort items
|
||||
private sortItems() {
|
||||
this.items.sort((a, b) => {
|
||||
if (a.date && b.date) {
|
||||
return b.date - a.date;
|
||||
} else if (!a.date) {
|
||||
return -1;
|
||||
} else if (!b.date) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onUpdateTopics(topicId: string) {
|
||||
const items = this.items.filter(i => i.id === topicId);
|
||||
const topic = TopicManager.getInstance().getTopic(topicId);
|
||||
items.map((item) => {
|
||||
item.label = topic?.name;
|
||||
item.date = topic?.lastUpdated;
|
||||
});
|
||||
this.sortItems();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
onReloadTopics(topics: Topic[]) {
|
||||
const items = topics.map((topic) => {
|
||||
return new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None);
|
||||
});
|
||||
this.items = items;
|
||||
this.sortItems();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
onDeleteTopic(topicId: string) {
|
||||
this.items = this.items.filter(i => i.id !== topicId);
|
||||
this.sortItems();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
setSelectedItem(item: TopicTreeItem): void {
|
||||
this.items.map((item) => {
|
||||
item.uncheck();
|
||||
});
|
||||
item.check();
|
||||
this.selectedItem = item;
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
getChildren(element?: TopicTreeItem): vscode.ProviderResult<TopicTreeItem[]> {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
getTreeItem(element: TopicTreeItem): vscode.TreeItem | Thenable<vscode.TreeItem> {
|
||||
element.command = {
|
||||
title: 'Select Item',
|
||||
command: 'devchat-topicview.selectTopic',
|
||||
arguments: [element],
|
||||
};
|
||||
return element;
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
const topicList = TopicManager.getInstance().getTopicList();
|
||||
this.onReloadTopics(topicList);
|
||||
}
|
||||
|
||||
addItem(topic: Topic): void {
|
||||
const newItem = new TopicTreeItem(topic.name ? topic.name : "new topic", topic.topicId, topic.lastUpdated, vscode.TreeItemCollapsibleState.None);
|
||||
this.items.push(newItem);
|
||||
this.sortItems();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
|
||||
deleteItem(item: TopicTreeItem): void {
|
||||
this.items = this.items.filter(i => i !== item);
|
||||
this.sortItems();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
// devchat.ts
|
||||
import * as vscode from 'vscode';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
@ -7,6 +6,8 @@ import * as fs from 'fs';
|
||||
import { logger } from '../util/logger';
|
||||
import { CommandRun } from "../util/commonUtil";
|
||||
import ExtensionContextHolder from '../util/extensionContext';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
import { ApiKeyManager } from '../util/apiKey';
|
||||
|
||||
|
||||
|
||||
@ -84,18 +85,6 @@ class DevChat {
|
||||
return args;
|
||||
}
|
||||
|
||||
async getOpenaiApiKey(): Promise<string | undefined> {
|
||||
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets;
|
||||
let openaiApiKey = await secretStorage.get("devchat_OPENAI_API_KEY");
|
||||
if (!openaiApiKey) {
|
||||
openaiApiKey = vscode.workspace.getConfiguration('DevChat').get('API_KEY');
|
||||
}
|
||||
if (!openaiApiKey) {
|
||||
openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
}
|
||||
return openaiApiKey;
|
||||
}
|
||||
|
||||
private parseOutData(stdout: string, isPartial: boolean): ChatResponse {
|
||||
const responseLines = stdout.trim().split("\n");
|
||||
|
||||
@ -148,15 +137,7 @@ class DevChat {
|
||||
}
|
||||
|
||||
apiEndpoint(apiKey: string | undefined): any {
|
||||
let openAiApiBase: string | undefined = undefined;
|
||||
if (apiKey?.startsWith("DC.")) {
|
||||
// TODO add devchat proxy
|
||||
openAiApiBase = "https://xw4ymuy6qj.ap-southeast-1.awsapprunner.com/api/v1";
|
||||
}
|
||||
|
||||
if (vscode.workspace.getConfiguration('DevChat').get('API_ENDPOINT')) {
|
||||
openAiApiBase = vscode.workspace.getConfiguration('DevChat').get('API_ENDPOINT');
|
||||
}
|
||||
const openAiApiBase = ApiKeyManager.getEndPoint(apiKey);
|
||||
|
||||
const openAiApiBaseObject = openAiApiBase ? { OPENAI_API_BASE: openAiApiBase } : {};
|
||||
return openAiApiBaseObject;
|
||||
@ -166,8 +147,8 @@ class DevChat {
|
||||
const args = await this.buildArgs(options);
|
||||
args.push(content);
|
||||
|
||||
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
|
||||
let openaiApiKey = await this.getOpenaiApiKey();
|
||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
let openaiApiKey = await ApiKeyManager.getApiKey();
|
||||
if (!openaiApiKey) {
|
||||
logger.channel()?.error('OpenAI key is invalid!');
|
||||
logger.channel()?.show();
|
||||
@ -177,13 +158,13 @@ class DevChat {
|
||||
// 如果配置了devchat的TOKEN,那么就需要使用默认的代理
|
||||
let openAiApiBaseObject = this.apiEndpoint(openaiApiKey);
|
||||
|
||||
const openaiModel = vscode.workspace.getConfiguration('DevChat').get('OpenAI.model');
|
||||
const openaiTemperature = vscode.workspace.getConfiguration('DevChat').get('OpenAI.temperature');
|
||||
const openaiStream = vscode.workspace.getConfiguration('DevChat').get('OpenAI.stream');
|
||||
const llmModel = vscode.workspace.getConfiguration('DevChat').get('llmModel');
|
||||
const tokensPerPrompt = vscode.workspace.getConfiguration('DevChat').get('OpenAI.tokensPerPrompt');
|
||||
const openaiModel = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.model');
|
||||
const openaiTemperature = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.temperature');
|
||||
const openaiStream = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.stream');
|
||||
const llmModel = UiUtilWrapper.getConfiguration('DevChat', 'llmModel');
|
||||
const tokensPerPrompt = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.tokensPerPrompt');
|
||||
|
||||
let devChat: string | undefined = vscode.workspace.getConfiguration('DevChat').get('DevChatPath');
|
||||
let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath');
|
||||
if (!devChat) {
|
||||
devChat = 'devchat';
|
||||
}
|
||||
@ -227,12 +208,11 @@ class DevChat {
|
||||
const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnAsyncOptions, onStdoutPartial, undefined, undefined, undefined);
|
||||
|
||||
if (stderr) {
|
||||
const errorMessage = stderr.trim().match(/Error:(.+)/)?.[1];
|
||||
return {
|
||||
"prompt-hash": "",
|
||||
user: "",
|
||||
date: "",
|
||||
response: errorMessage ? `Error: ${errorMessage}` : "Unknown error",
|
||||
response: stderr,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
@ -253,7 +233,7 @@ class DevChat {
|
||||
async log(options: LogOptions = {}): Promise<LogEntry[]> {
|
||||
const args = this.buildLogArgs(options);
|
||||
const devChat = this.getDevChatPath();
|
||||
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
|
||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
const openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
|
||||
logger.channel()?.info(`Running devchat with args: ${args.join(" ")}`);
|
||||
@ -286,7 +266,7 @@ class DevChat {
|
||||
if (options.maxCount) {
|
||||
args.push('--max-count', `${options.maxCount}`);
|
||||
} else {
|
||||
const maxLogCount = vscode.workspace.getConfiguration('DevChat').get('maxLogCount');
|
||||
const maxLogCount = UiUtilWrapper.getConfiguration('DevChat', 'maxLogCount');
|
||||
args.push('--max-count', `${maxLogCount}`);
|
||||
}
|
||||
|
||||
@ -294,7 +274,7 @@ class DevChat {
|
||||
}
|
||||
|
||||
private getDevChatPath(): string {
|
||||
let devChat: string | undefined = vscode.workspace.getConfiguration('DevChat').get('DevChatPath');
|
||||
let devChat: string | undefined = UiUtilWrapper.getConfiguration('DevChat', 'DevChatPath');
|
||||
if (!devChat) {
|
||||
devChat = 'devchat';
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { spawn } from "child_process";
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { logger } from "../util/logger";
|
||||
import { CommandRun } from "../util/commonUtil";
|
||||
import { UiUtilWrapper } from "../util/uiUtil";
|
||||
|
||||
interface DtmResponse {
|
||||
status: number;
|
||||
@ -17,7 +17,7 @@ class DtmWrapper {
|
||||
private commandRun: CommandRun;
|
||||
|
||||
constructor() {
|
||||
this.workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath || '.';
|
||||
this.workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath() || '.';
|
||||
this.commandRun = new CommandRun();
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import DevChat, { LogEntry, LogOptions } from '../toolwrapper/devchat';
|
||||
|
||||
import { loadTopicList } from './loadTopics';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
export class Topic {
|
||||
name: string | undefined;
|
||||
@ -194,7 +194,7 @@ export class TopicManager {
|
||||
|
||||
if (topic.firstMessageHash) {
|
||||
// get ${WORKSPACE_ROOT}/.chat/.deletedTopics
|
||||
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
|
||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
const deletedTopicsPath = path.join(workspaceDir!, '.chat', '.deletedTopics');
|
||||
|
||||
// read ${WORKSPACE_ROOT}/.chat/.deletedTopics as String[]
|
||||
@ -218,7 +218,7 @@ export class TopicManager {
|
||||
}
|
||||
|
||||
isDeleteTopic(topicId: string) {
|
||||
const workspaceDir = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
|
||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
const deletedTopicsPath = path.join(workspaceDir!, '.chat', '.deletedTopics');
|
||||
|
||||
if (!fs.existsSync(deletedTopicsPath)) {
|
||||
|
41
src/util/apiKey.ts
Normal file
41
src/util/apiKey.ts
Normal file
@ -0,0 +1,41 @@
|
||||
// src/apiKey.ts
|
||||
|
||||
import { UiUtilWrapper } from './uiUtil';
|
||||
|
||||
export class ApiKeyManager {
|
||||
static async getApiKey(): Promise<string | undefined> {
|
||||
let apiKey = await UiUtilWrapper.secretStorageGet("devchat_OPENAI_API_KEY");
|
||||
if (!apiKey) {
|
||||
apiKey = UiUtilWrapper.getConfiguration('DevChat', 'API_KEY');
|
||||
}
|
||||
if (!apiKey) {
|
||||
apiKey = process.env.OPENAI_API_KEY;
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
static getKeyType(apiKey: string): string | undefined {
|
||||
if (apiKey.startsWith("sk.")) {
|
||||
return "sk";
|
||||
} else if (apiKey.startsWith("DC.")) {
|
||||
return "DC";
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static async writeApiKeySecret(apiKey: string): Promise<void> {
|
||||
await UiUtilWrapper.storeSecret("devchat_OPENAI_API_KEY", apiKey);
|
||||
}
|
||||
|
||||
static getEndPoint(apiKey: string | undefined): string | undefined {
|
||||
let endPoint = UiUtilWrapper.getConfiguration('DevChat', 'API_ENDPOINT');
|
||||
if (!endPoint) {
|
||||
endPoint = process.env.OPENAI_API_BASE;
|
||||
}
|
||||
if (!endPoint && apiKey?.startsWith("DC.")) {
|
||||
endPoint = "https://xw4ymuy6qj.ap-southeast-1.awsapprunner.com/api/v1";
|
||||
}
|
||||
return endPoint;
|
||||
}
|
||||
}
|
@ -19,9 +19,10 @@ function findMatchingIndex(list1: string[], list2: string[]): number[] {
|
||||
let isMatch = true;
|
||||
for (let j = 0; j < list2.length; j++) {
|
||||
if (list1[i + j].trim() !== list2[j].trim()) {
|
||||
if (j > 0) {
|
||||
|
||||
//if (j > 0) {
|
||||
logger.channel()?.info(`findMatchingIndex end at ${j} ${list1[i + j].trim()} != ${list2[j].trim()}`);
|
||||
}
|
||||
//}
|
||||
|
||||
isMatch = false;
|
||||
break;
|
||||
|
@ -1,11 +1,13 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as childProcess from 'child_process';
|
||||
|
||||
import { parseArgsStringToArgv } from 'string-argv';
|
||||
|
||||
import { logger } from './logger';
|
||||
import { spawn, exec } from 'child_process';
|
||||
import { UiUtilWrapper } from './uiUtil';
|
||||
|
||||
export function createTempSubdirectory(subdir: string): string {
|
||||
// 获取系统临时目录
|
||||
@ -77,7 +79,7 @@ export class CommandRun {
|
||||
if (code === 0) {
|
||||
resolve({ exitCode: code, stdout, stderr });
|
||||
} else {
|
||||
reject({ exitCode: code, stdout, stderr });
|
||||
resolve({ exitCode: code, stdout, stderr });
|
||||
}
|
||||
});
|
||||
|
||||
@ -90,7 +92,7 @@ export class CommandRun {
|
||||
logger.channel()?.error(`Error occurred: ${error.message}`);
|
||||
logger.channel()?.show();
|
||||
}
|
||||
reject({ exitCode: error.code, stdout: "", stderr: error.message });
|
||||
resolve({ exitCode: error.code, stdout: "", stderr: error.message });
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -110,7 +112,7 @@ export async function runCommandAndWriteOutput(
|
||||
): Promise<CommandResult> {
|
||||
const run = new CommandRun();
|
||||
const options = {
|
||||
cwd: vscode.workspace.workspaceFolders?.[0].uri.fsPath || '.',
|
||||
cwd: UiUtilWrapper.workspaceFoldersFirstPath() || '.',
|
||||
};
|
||||
|
||||
return run.spawnAsync(command, args, options, undefined, undefined, undefined, outputFile);
|
||||
@ -122,7 +124,7 @@ export async function runCommandStringAndWriteOutput(
|
||||
): Promise<CommandResult> {
|
||||
const run = new CommandRun();
|
||||
const options = {
|
||||
cwd: vscode.workspace.workspaceFolders?.[0].uri.fsPath || '.'
|
||||
cwd: UiUtilWrapper.workspaceFoldersFirstPath() || '.'
|
||||
};
|
||||
|
||||
// Split the commandString into command and args array using string-argv
|
||||
@ -143,13 +145,14 @@ export async function runCommandStringAndWriteOutput(
|
||||
|
||||
export async function getLanguageIdByFileName(fileName: string): Promise<string | undefined> {
|
||||
try {
|
||||
// 打开指定的文件
|
||||
const document = await vscode.workspace.openTextDocument(fileName);
|
||||
// 获取文件的语言标识符
|
||||
const languageId = document.languageId;
|
||||
const languageId = await UiUtilWrapper.languageId(fileName);
|
||||
return languageId;
|
||||
} catch (error) {
|
||||
// 如果无法打开文件或发生其他错误,返回undefined
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function runCommand(command: string): string {
|
||||
return childProcess.execSync(command).toString();
|
||||
}
|
@ -1,12 +1,19 @@
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
export interface LogChannel {
|
||||
info(message: string, ...args: any[]): void;
|
||||
warn(message: string, ...args: any[]): void;
|
||||
error(message: string | Error, ...args: any[]): void;
|
||||
debug(message: string, ...args: any[]): void;
|
||||
show(): void;
|
||||
}
|
||||
|
||||
export class logger {
|
||||
private static _channel: vscode.LogOutputChannel | undefined;
|
||||
public static init(context: vscode.ExtensionContext): void {
|
||||
this._channel = vscode.window.createOutputChannel('DevChat', { log: true });
|
||||
private static _channel: LogChannel | undefined;
|
||||
public static init(channel: LogChannel): void {
|
||||
this._channel = channel;
|
||||
}
|
||||
|
||||
public static channel(): vscode.LogOutputChannel | undefined {
|
||||
public static channel(): LogChannel | undefined {
|
||||
return this._channel;
|
||||
}
|
||||
}
|
||||
|
39
src/util/logger_vscode.ts
Normal file
39
src/util/logger_vscode.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { LogChannel } from "./logger";
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class LoggerChannelVscode implements LogChannel {
|
||||
_channel: vscode.LogOutputChannel;
|
||||
|
||||
private static _instance: LoggerChannelVscode;
|
||||
|
||||
private constructor() {
|
||||
this._channel = vscode.window.createOutputChannel('DevChat', { log: true });
|
||||
}
|
||||
|
||||
public static getInstance(): LoggerChannelVscode {
|
||||
if (!this._instance) {
|
||||
this._instance = new LoggerChannelVscode();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
info(message: string, ...args: any[]): void {
|
||||
this._channel.info(message, ...args);
|
||||
}
|
||||
|
||||
warn(message: string, ...args: any[]): void {
|
||||
this._channel.warn(message, ...args);
|
||||
}
|
||||
|
||||
error(message: string | Error, ...args: any[]): void {
|
||||
this._channel.error(message, ...args);
|
||||
}
|
||||
|
||||
debug(message: string, ...args: any[]): void {
|
||||
this._channel.debug(message, ...args);
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this._channel.show();
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
class MessageHistory {
|
||||
export class MessageHistory {
|
||||
private history: any[];
|
||||
private lastmessage: any | null;
|
||||
|
||||
@ -21,10 +20,6 @@ class MessageHistory {
|
||||
return this.lastmessage;
|
||||
}
|
||||
|
||||
remove() {
|
||||
return;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.history = [];
|
||||
this.lastmessage = null;
|
||||
|
48
src/util/uiUtil.ts
Normal file
48
src/util/uiUtil.ts
Normal file
@ -0,0 +1,48 @@
|
||||
|
||||
export interface UiUtil {
|
||||
languageId(uri: string): Promise<string>;
|
||||
workspaceFoldersFirstPath(): string | undefined;
|
||||
getConfiguration(key1: string, key2: string): string | undefined;
|
||||
secretStorageGet(key: string): Promise<string | undefined>;
|
||||
writeFile(uri: string, content: string): Promise<void>;
|
||||
showInputBox(option: object): Promise<string | undefined>;
|
||||
storeSecret(key: string, value: string): Promise<void>;
|
||||
extensionPath(): string;
|
||||
runTerminal(terminalName:string, command: string): void;
|
||||
}
|
||||
|
||||
export class UiUtilWrapper {
|
||||
private static _uiUtil: UiUtil | undefined;
|
||||
public static init(uiUtil: UiUtil): void {
|
||||
this._uiUtil = uiUtil;
|
||||
}
|
||||
|
||||
public static async languageId(uri: string): Promise<string | undefined> {
|
||||
return await this._uiUtil?.languageId(uri);
|
||||
}
|
||||
public static workspaceFoldersFirstPath(): string | undefined {
|
||||
return this._uiUtil?.workspaceFoldersFirstPath();
|
||||
}
|
||||
public static getConfiguration(key1: string, key2: string): string | undefined {
|
||||
return this._uiUtil?.getConfiguration(key1, key2);
|
||||
}
|
||||
public static async secretStorageGet(key: string): Promise<string | undefined> {
|
||||
return await this._uiUtil?.secretStorageGet(key);
|
||||
}
|
||||
public static async writeFile(uri: string, content: string): Promise<void> {
|
||||
return await this._uiUtil?.writeFile(uri, content);
|
||||
}
|
||||
public static async showInputBox(option: object): Promise<string | undefined> {
|
||||
return await this._uiUtil?.showInputBox(option);
|
||||
}
|
||||
public static async storeSecret(key: string, value: string): Promise<void> {
|
||||
return await this._uiUtil?.storeSecret(key, value);
|
||||
}
|
||||
public static extensionPath(): string {
|
||||
return this._uiUtil?.extensionPath()!;
|
||||
}
|
||||
public static runTerminal(terminalName: string, command: string): void {
|
||||
this._uiUtil?.runTerminal(terminalName, command);
|
||||
}
|
||||
}
|
||||
|
42
src/util/uiUtil_vscode.ts
Normal file
42
src/util/uiUtil_vscode.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import ExtensionContextHolder from './extensionContext';
|
||||
import { UiUtil } from './uiUtil';
|
||||
|
||||
|
||||
export class UiUtilVscode implements UiUtil {
|
||||
public async languageId(uri: string): Promise<string> {
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
return document.languageId;
|
||||
}
|
||||
public workspaceFoldersFirstPath(): string | undefined {
|
||||
return vscode.workspace.workspaceFolders?.[0].uri.fsPath;
|
||||
}
|
||||
|
||||
public getConfiguration(key1: string, key2: string): string | undefined {
|
||||
return vscode.workspace.getConfiguration(key1).get(key2);
|
||||
}
|
||||
public async secretStorageGet(key: string): Promise<string | undefined> {
|
||||
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets;
|
||||
let openaiApiKey = await secretStorage.get(key);
|
||||
return openaiApiKey;
|
||||
}
|
||||
public async writeFile(uri: string, content: string): Promise<void> {
|
||||
await vscode.workspace.fs.writeFile(vscode.Uri.file(uri), Buffer.from(content));
|
||||
}
|
||||
public async showInputBox(option: object): Promise<string | undefined> {
|
||||
return vscode.window.showInputBox(option);
|
||||
}
|
||||
public async storeSecret(key: string, value: string): Promise<void> {
|
||||
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets;
|
||||
await secretStorage.store(key, value);
|
||||
}
|
||||
public extensionPath(): string {
|
||||
return ExtensionContextHolder.context!.extensionUri.fsPath;
|
||||
}
|
||||
public runTerminal(terminalName: string, command: string): void {
|
||||
const terminal = vscode.window.createTerminal(terminalName);
|
||||
terminal.sendText(command);
|
||||
terminal.show();
|
||||
}
|
||||
}
|
76
test/command/commandManager.test.ts
Normal file
76
test/command/commandManager.test.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import CommandManager, { Command } from '../../src/command/commandManager';
|
||||
import CustomCommands, { Command as CCommand} from '../../src/command/customCommand';
|
||||
|
||||
describe('CommandManager', () => {
|
||||
let commandManager: CommandManager;
|
||||
|
||||
beforeEach(() => {
|
||||
commandManager = CommandManager.getInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset the command list after each test
|
||||
commandManager['commands'] = [];
|
||||
});
|
||||
|
||||
it('should register a command', () => {
|
||||
const command: Command = {
|
||||
name: 'test',
|
||||
pattern: 'test',
|
||||
description: 'Test command',
|
||||
handler: async (commandName: string, userInput: string) => {
|
||||
return 'Test result';
|
||||
},
|
||||
};
|
||||
|
||||
commandManager.registerCommand(command);
|
||||
expect(commandManager['commands']).to.include(command);
|
||||
});
|
||||
|
||||
it('should return the command list', () => {
|
||||
const command: Command = {
|
||||
name: 'test',
|
||||
pattern: 'test',
|
||||
description: 'Test command',
|
||||
handler: async (commandName: string, userInput: string) => {
|
||||
return 'Test result';
|
||||
},
|
||||
};
|
||||
|
||||
commandManager.registerCommand(command);
|
||||
expect(commandManager.getCommandList()).to.include(command);
|
||||
});
|
||||
|
||||
it('should process text with a command', async () => {
|
||||
const command: Command = {
|
||||
name: 'test',
|
||||
pattern: 'test',
|
||||
description: 'Test command',
|
||||
handler: async (commandName: string, userInput: string) => {
|
||||
return 'Test result';
|
||||
},
|
||||
};
|
||||
|
||||
commandManager.registerCommand(command);
|
||||
const result = await commandManager.processText('/test');
|
||||
expect(result).to.equal('Test result');
|
||||
});
|
||||
|
||||
it('should process text with a custom command', async () => {
|
||||
const customCommand: CCommand = {
|
||||
name: 'customTest',
|
||||
pattern: 'customTest',
|
||||
description: 'Custom test command',
|
||||
message: 'Custom test result',
|
||||
show: true,
|
||||
default: false,
|
||||
instructions: []
|
||||
};
|
||||
|
||||
CustomCommands.getInstance().regCommand(customCommand);
|
||||
const result = await commandManager.processText('/customTest');
|
||||
expect(result).to.equal(' Custom test result');
|
||||
});
|
||||
});
|
106
test/command/customCommand.test.ts
Normal file
106
test/command/customCommand.test.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import mockFs from 'mock-fs';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import CustomCommands, { Command } from '../../src/command/customCommand';
|
||||
|
||||
|
||||
describe('CustomCommands', () => {
|
||||
let customCommands: CustomCommands;
|
||||
|
||||
beforeEach(() => {
|
||||
customCommands = CustomCommands.getInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset the command list after each test
|
||||
customCommands['commands'] = [];
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should parse commands from workflows directory', () => {
|
||||
// Mock the file system with two directories, one with _setting_.json and one without
|
||||
mockFs({
|
||||
'workflows': {
|
||||
'command1': {
|
||||
'_setting_.json': JSON.stringify({
|
||||
pattern: 'command1',
|
||||
description: 'Command 1',
|
||||
message: 'Command 1 message',
|
||||
default: false,
|
||||
show: true,
|
||||
instructions: ['instruction1', 'instruction2'],
|
||||
}),
|
||||
},
|
||||
'command2': {
|
||||
// No _setting_.json file
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const workflowsDir = path.join(process.cwd(), 'workflows');
|
||||
customCommands.parseCommands(workflowsDir);
|
||||
|
||||
const expectedResult: Command[] = [
|
||||
{
|
||||
name: 'command1',
|
||||
pattern: 'command1',
|
||||
description: 'Command 1',
|
||||
message: 'Command 1 message',
|
||||
default: false,
|
||||
show: true,
|
||||
instructions: ['instruction1', 'instruction2'],
|
||||
},
|
||||
];
|
||||
|
||||
expect(customCommands['commands']).to.deep.equal(expectedResult);
|
||||
});
|
||||
|
||||
it('should register a custom command', () => {
|
||||
const command: Command = {
|
||||
name: 'test',
|
||||
pattern: 'test',
|
||||
description: 'Test command',
|
||||
message: 'Test message',
|
||||
default: false,
|
||||
show: true,
|
||||
instructions: ['instruction1', 'instruction2'],
|
||||
};
|
||||
|
||||
customCommands.regCommand(command);
|
||||
expect(customCommands['commands']).to.include(command);
|
||||
});
|
||||
|
||||
it('should get a custom command by name', () => {
|
||||
const command: Command = {
|
||||
name: 'test',
|
||||
pattern: 'test',
|
||||
description: 'Test command',
|
||||
message: 'Test message',
|
||||
default: false,
|
||||
show: true,
|
||||
instructions: ['instruction1', 'instruction2'],
|
||||
};
|
||||
|
||||
customCommands.regCommand(command);
|
||||
const foundCommand = customCommands.getCommand('test');
|
||||
expect(foundCommand).to.deep.equal(command);
|
||||
});
|
||||
|
||||
it('should handle a custom command', () => {
|
||||
const command: Command = {
|
||||
name: 'test',
|
||||
pattern: 'test',
|
||||
description: 'Test command',
|
||||
message: 'Test message',
|
||||
default: false,
|
||||
show: true,
|
||||
instructions: ['instruction1', 'instruction2'],
|
||||
};
|
||||
|
||||
customCommands.regCommand(command);
|
||||
const result = customCommands.handleCommand('test');
|
||||
expect(result).to.equal('[instruction|./.chat/workflows/test/instruction1] [instruction|./.chat/workflows/test/instruction2] Test message');
|
||||
});
|
||||
});
|
43
test/context/contextCodeSelected.test.ts
Normal file
43
test/context/contextCodeSelected.test.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, afterEach, beforeEach } from 'mocha';
|
||||
import { handleCodeSelected } from '../../src/context/contextCodeSelected';
|
||||
import * as path from 'path';
|
||||
import { UiUtilWrapper } from '../../src/util/uiUtil';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('handleCodeSelected', () => {
|
||||
let languageIdStub: sinon.SinonStub;
|
||||
let workspaceFoldersFirstPathStub: sinon.SinonStub;
|
||||
let writeFileStub: sinon.SinonStub;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock UiUtilWrapper functions
|
||||
languageIdStub = sinon.stub(UiUtilWrapper, 'languageId').resolves('typescript');
|
||||
workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath').returns('test');
|
||||
writeFileStub = sinon.stub(UiUtilWrapper, 'writeFile').resolves();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore the original functions after each test
|
||||
languageIdStub.restore();
|
||||
workspaceFoldersFirstPathStub.restore();
|
||||
writeFileStub.restore();
|
||||
});
|
||||
|
||||
it('should create a context file with the correct content', async () => {
|
||||
const fileSelected = path.join(__dirname, 'testFile.ts');
|
||||
const codeSelected = 'console.log("Hello, world!");';
|
||||
|
||||
const contextFile = await handleCodeSelected(fileSelected, codeSelected);
|
||||
|
||||
// Check if the mocked functions were called with the correct arguments
|
||||
expect(languageIdStub.calledWith(fileSelected)).to.be.true;
|
||||
expect(workspaceFoldersFirstPathStub.called).to.be.true;
|
||||
expect(writeFileStub.called).to.be.true;
|
||||
|
||||
// Extract the temp file path from the context string
|
||||
const tempFilePath = contextFile.match(/\[context\|(.*?)\]/)?.[1];
|
||||
|
||||
expect(tempFilePath).to.not.be.undefined;
|
||||
});
|
||||
});
|
18
test/context/loadContexts.test.ts
Normal file
18
test/context/loadContexts.test.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import '../../src/context/loadContexts';
|
||||
import ChatContextManager from '../../src/context/contextManager';
|
||||
import { gitDiffCachedContext } from '../../src/context/contextGitDiffCached';
|
||||
import { gitDiffContext } from '../../src/context/contextGitDiff';
|
||||
import { customCommandContext } from '../../src/context/contextCustomCommand';
|
||||
|
||||
describe('loadContexts', () => {
|
||||
it('should register all contexts', () => {
|
||||
const chatContextManager = ChatContextManager.getInstance();
|
||||
const contextList = chatContextManager.getContextList();
|
||||
|
||||
expect(contextList).to.include(gitDiffCachedContext);
|
||||
expect(contextList).to.include(gitDiffContext);
|
||||
expect(contextList).to.include(customCommandContext);
|
||||
});
|
||||
});
|
84
test/contributes/commandsBase.test.ts
Normal file
84
test/contributes/commandsBase.test.ts
Normal file
@ -0,0 +1,84 @@
|
||||
// test/commandsBase.test.ts
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as commonUtil from '../../src/util/commonUtil';
|
||||
import * as commandsBase from '../../src/contributes/commandsBase';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('commandsBase', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('checkDevChatDependency', () => {
|
||||
it('should return true if DevChat is installed', () => {
|
||||
sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => {
|
||||
if (command === 'python3 -m pipx environment') {
|
||||
return 'PIPX_BIN_DIR=/path/to/bin';
|
||||
} else if (command === 'devchat --help') {
|
||||
return 'DevChat help text';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const result = commandsBase.checkDevChatDependency();
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if DevChat is not installed', () => {
|
||||
sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => {
|
||||
if (command === 'python3 -m pipx environment') {
|
||||
return 'PIPX_BIN_DIR=/path/to/bin';
|
||||
} else if (command === 'devchat --help') {
|
||||
throw new Error('Command not found');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const result = commandsBase.checkDevChatDependency();
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if pipx environment is not found', () => {
|
||||
sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => {
|
||||
if (command === 'python3 -m pipx environment') {
|
||||
return 'No pipx environment found';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const result = commandsBase.checkDevChatDependency();
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPipxEnvironmentPath', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('should return the pipx environment path if found', () => {
|
||||
sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => {
|
||||
if (command === 'python3 -m pipx environment') {
|
||||
return 'PIPX_BIN_DIR=/path/to/bin';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const result = commandsBase.getPipxEnvironmentPath();
|
||||
expect(result).to.equal('/path/to/bin');
|
||||
});
|
||||
|
||||
it('should return null if pipx environment path is not found', () => {
|
||||
sinon.stub(commonUtil, 'runCommand').callsFake((command: string) => {
|
||||
if (command === 'python3 -m pipx environment') {
|
||||
return 'No pipx environment found';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const result = commandsBase.getPipxEnvironmentPath();
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
309
test/handler/sendMessageBase.test.ts
Normal file
309
test/handler/sendMessageBase.test.ts
Normal file
@ -0,0 +1,309 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import { Context } from 'mocha';
|
||||
import sinon from 'sinon';
|
||||
import { parseMessage, getInstructionFiles, parseMessageAndSetOptions, getParentHash, handleTopic, handlerResponseText, sendMessageBase, stopDevChatBase } from '../../src/handler/sendMessageBase';
|
||||
import DevChat, { ChatResponse } from '../../src/toolwrapper/devchat';
|
||||
import CommandManager from '../../src/command/commandManager';
|
||||
import messageHistory from '../../src/util/messageHistory';
|
||||
import { TopicManager } from '../../src/topic/topicManager';
|
||||
import CustomCommands from '../../src/command/customCommand';
|
||||
import { UiUtilWrapper } from '../../src/util/uiUtil';
|
||||
|
||||
|
||||
describe('sendMessageBase', () => {
|
||||
let workspaceFoldersFirstPathStub: sinon.SinonStub;
|
||||
let getConfigurationStub: sinon.SinonStub;
|
||||
|
||||
beforeEach(() => {
|
||||
workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath');
|
||||
getConfigurationStub = sinon.stub(UiUtilWrapper, 'getConfiguration');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
workspaceFoldersFirstPathStub.restore();
|
||||
getConfigurationStub.restore();
|
||||
});
|
||||
|
||||
describe('parseMessage', () => {
|
||||
it('should parse message correctly', () => {
|
||||
const message = '[context|path/to/context] [instruction|path/to/instruction] [reference|path/to/reference] Hello, world!';
|
||||
const result = parseMessage(message);
|
||||
|
||||
expect(result.context).to.deep.equal(['path/to/context']);
|
||||
expect(result.instruction).to.deep.equal(['path/to/instruction']);
|
||||
expect(result.reference).to.deep.equal(['path/to/reference']);
|
||||
expect(result.text).to.equal('Hello, world!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInstructionFiles', () => {
|
||||
it('should return instruction files', () => {
|
||||
const result = getInstructionFiles();
|
||||
expect(result).to.be.an('array');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseMessageAndSetOptions', () => {
|
||||
it('should parse message and set options correctly', async () => {
|
||||
const message = {
|
||||
text: '[context|path/to/context] [instruction|path/to/instruction] [reference|path/to/reference] Hello, world!'
|
||||
};
|
||||
const chatOptions: any = {};
|
||||
|
||||
const result = await parseMessageAndSetOptions(message, chatOptions);
|
||||
|
||||
expect(result.context).to.deep.equal(['path/to/context']);
|
||||
expect(result.instruction).to.deep.equal(['path/to/instruction']);
|
||||
expect(result.reference).to.deep.equal(['path/to/reference']);
|
||||
expect(result.text).to.equal('Hello, world!');
|
||||
expect(chatOptions.context).to.deep.equal(['path/to/context']);
|
||||
expect(chatOptions.header).to.deep.equal(['path/to/instruction']);
|
||||
expect(chatOptions.reference).to.deep.equal(['path/to/reference']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getParentHash', () => {
|
||||
beforeEach(() => {
|
||||
messageHistory.clear();
|
||||
});
|
||||
|
||||
it('should return parent hash when message hash is provided and found in history', () => {
|
||||
const message1 = {
|
||||
hash: 'somehash1',
|
||||
parentHash: 'parentHash1'
|
||||
};
|
||||
const message2 = {
|
||||
hash: 'somehash2',
|
||||
parentHash: 'parentHash2'
|
||||
};
|
||||
messageHistory.add(message1);
|
||||
messageHistory.add(message2);
|
||||
|
||||
const message = {
|
||||
hash: 'somehash1'
|
||||
};
|
||||
|
||||
const result = getParentHash(message);
|
||||
expect(result).to.equal('parentHash1');
|
||||
});
|
||||
|
||||
it('should return undefined when message hash is provided but not found in history', () => {
|
||||
const message1 = {
|
||||
hash: 'somehash1',
|
||||
parentHash: 'parentHash1'
|
||||
};
|
||||
const message2 = {
|
||||
hash: 'somehash2',
|
||||
parentHash: 'parentHash2'
|
||||
};
|
||||
messageHistory.add(message1);
|
||||
messageHistory.add(message2);
|
||||
|
||||
const message = {
|
||||
hash: 'nonexistenthash'
|
||||
};
|
||||
|
||||
const result = getParentHash(message);
|
||||
expect(result).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should return last message hash when message hash is not provided', () => {
|
||||
const message1 = {
|
||||
hash: 'somehash1',
|
||||
parentHash: 'parentHash1'
|
||||
};
|
||||
const message2 = {
|
||||
hash: 'somehash2',
|
||||
parentHash: 'parentHash2'
|
||||
};
|
||||
messageHistory.add(message1);
|
||||
messageHistory.add(message2);
|
||||
|
||||
const message = {};
|
||||
|
||||
const result = getParentHash(message);
|
||||
expect(result).to.equal('somehash2');
|
||||
});
|
||||
|
||||
it('should return undefined when message hash is not provided and history is empty', () => {
|
||||
const message = {};
|
||||
|
||||
const result = getParentHash(message);
|
||||
expect(result).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleTopic', () => {
|
||||
it('should handle topic correctly', async () => {
|
||||
const parentHash = 'somehash';
|
||||
const message = {
|
||||
text: 'Hello, world!'
|
||||
};
|
||||
const chatResponse: ChatResponse = {
|
||||
response: 'Hello, user!',
|
||||
isError: false,
|
||||
user: 'user',
|
||||
date: '2022-01-01T00:00:00.000Z',
|
||||
'prompt-hash': 'responsehash'
|
||||
};
|
||||
|
||||
await handleTopic(parentHash, message, chatResponse);
|
||||
// Check if the topic was updated correctly
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlerResponseText', () => {
|
||||
it('should handle response text correctly when isError is false', async () => {
|
||||
const partialDataText = 'Partial data';
|
||||
const chatResponse: ChatResponse = {
|
||||
response: 'Hello, user!',
|
||||
isError: false,
|
||||
user: 'user',
|
||||
date: '2022-01-01T00:00:00.000Z',
|
||||
'prompt-hash': 'responsehash'
|
||||
};
|
||||
|
||||
const result = await handlerResponseText(partialDataText, chatResponse);
|
||||
expect(result).to.equal('Hello, user!');
|
||||
});
|
||||
|
||||
it('should handle response text correctly when isError is true', async () => {
|
||||
const partialDataText = 'Partial data';
|
||||
const chatResponse: ChatResponse = {
|
||||
response: 'Error occurred!',
|
||||
isError: true,
|
||||
user: 'user',
|
||||
date: '2022-01-01T00:00:00.000Z',
|
||||
'prompt-hash': 'responsehash'
|
||||
};
|
||||
|
||||
const result = await handlerResponseText(partialDataText, chatResponse);
|
||||
expect(result).to.equal('Partial data\n\nError occurred!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendMessageBase', async () => {
|
||||
it('should send message correct with openai api key', async () => {
|
||||
const message = {
|
||||
text: 'Hello, world!'
|
||||
};
|
||||
const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => {
|
||||
// Handle partial data
|
||||
};
|
||||
|
||||
workspaceFoldersFirstPathStub.returns('./');
|
||||
|
||||
getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('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');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0);
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true');
|
||||
getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns(9000);
|
||||
|
||||
const result = await sendMessageBase(message, handlePartialData);
|
||||
expect(result).to.be.an('object');
|
||||
expect(result!.command).to.equal('receiveMessage');
|
||||
expect(result!.text).to.be.a('string');
|
||||
expect(result!.hash).to.be.a('string');
|
||||
expect(result!.user).to.be.a('string');
|
||||
expect(result!.date).to.be.a('string');
|
||||
expect(result!.isError).to.be.false;
|
||||
}).timeout(10000);
|
||||
|
||||
it('should send message correct with DevChat access key', async () => {
|
||||
const message = {
|
||||
text: 'Hello, world!'
|
||||
};
|
||||
const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => {
|
||||
// Handle partial data
|
||||
};
|
||||
|
||||
workspaceFoldersFirstPathStub.returns('./');
|
||||
|
||||
getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('DC.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOjY2MTI0NDU1ODE2LCJqdGkiOjcyMzc4ODIzMjI3Mjc4MzI2NTJ9.qGWJ_NyWjjj66oa5mbfi3Hjawe-Yp8syCDLkpyu4yS0');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0);
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true');
|
||||
getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns(9000);
|
||||
|
||||
const result = await sendMessageBase(message, handlePartialData);
|
||||
expect(result).to.be.an('object');
|
||||
expect(result!.command).to.equal('receiveMessage');
|
||||
expect(result!.text).to.be.a('string');
|
||||
expect(result!.hash).to.be.a('string');
|
||||
expect(result!.user).to.be.a('string');
|
||||
expect(result!.date).to.be.a('string');
|
||||
expect(result!.isError).to.be.false;
|
||||
}).timeout(10000);
|
||||
|
||||
it('should send message error with invalid api key', async () => {
|
||||
const message = {
|
||||
text: 'Hello, world!'
|
||||
};
|
||||
const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => {
|
||||
// Handle partial data
|
||||
};
|
||||
|
||||
workspaceFoldersFirstPathStub.returns('./');
|
||||
|
||||
getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('sk-KvH7ZCtHmFDCBTqH0jUv');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns('0');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true');
|
||||
getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns('9000');
|
||||
|
||||
const result = await sendMessageBase(message, handlePartialData);
|
||||
expect(result).to.be.an('object');
|
||||
expect(result!.command).to.equal('receiveMessage');
|
||||
expect(result!.text).to.be.a('string');
|
||||
expect(result!.hash).to.be.a('string');
|
||||
expect(result!.user).to.be.a('string');
|
||||
expect(result!.date).to.be.a('string');
|
||||
expect(result!.isError).to.be.true;
|
||||
}).timeout(10000);
|
||||
});
|
||||
|
||||
describe('stopDevChatBase', () => {
|
||||
it('should stop sendMessageBase correctly', async () => {
|
||||
const message = {
|
||||
text: 'Hello, world!'
|
||||
};
|
||||
const handlePartialData = (data: { command: string, text: string, user: string, date: string }) => {
|
||||
// Handle partial data
|
||||
};
|
||||
|
||||
workspaceFoldersFirstPathStub.returns('./');
|
||||
|
||||
getConfigurationStub.withArgs('DevChat', 'API_KEY').returns('DC.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOjY2MTI0NDU1ODE2LCJqdGkiOjcyMzc4ODIzMjI3Mjc4MzI2NTJ9.qGWJ_NyWjjj66oa5mbfi3Hjawe-Yp8syCDLkpyu4yS0');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.model').returns('gpt-4');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.temperature').returns(0);
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.stream').returns('true');
|
||||
getConfigurationStub.withArgs('DevChat', 'llmModel').returns('OpenAI');
|
||||
getConfigurationStub.withArgs('DevChat', 'OpenAI.tokensPerPrompt').returns(9000);
|
||||
|
||||
|
||||
// Start sendMessageBase in a separate Promise
|
||||
const sendMessagePromise = sendMessageBase(message, handlePartialData);
|
||||
|
||||
// Wait for a short period to ensure sendMessageBase has started
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Call stopDevChatBase
|
||||
const stopMessage = {
|
||||
text: 'stop'
|
||||
};
|
||||
await stopDevChatBase(stopMessage);
|
||||
|
||||
// Check if sendMessageBase has been stopped and returns an error
|
||||
try {
|
||||
const result = await sendMessagePromise;
|
||||
expect(result).to.undefined;
|
||||
} catch (error) {
|
||||
expect(error).to.be.an('error');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
60
test/toolwrapper/devchat.test.ts
Normal file
60
test/toolwrapper/devchat.test.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import sinon from 'sinon';
|
||||
import DevChat, { ChatOptions } from '../../src/toolwrapper/devchat';
|
||||
import { CommandRun } from '../../src/util/commonUtil';
|
||||
import { UiUtilWrapper } from '../../src/util/uiUtil';
|
||||
|
||||
describe('DevChat', () => {
|
||||
let devChat: DevChat;
|
||||
let spawnAsyncStub: sinon.SinonStub;
|
||||
let workspaceFoldersFirstPathStub: sinon.SinonStub;
|
||||
|
||||
beforeEach(() => {
|
||||
devChat = new DevChat();
|
||||
spawnAsyncStub = sinon.stub(CommandRun.prototype, 'spawnAsync');
|
||||
workspaceFoldersFirstPathStub = sinon.stub(UiUtilWrapper, 'workspaceFoldersFirstPath');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
spawnAsyncStub.restore();
|
||||
workspaceFoldersFirstPathStub.restore();
|
||||
});
|
||||
|
||||
|
||||
describe('chat', () => {
|
||||
it('should return a ChatResponse object with isError false when the chat is successful', async () => {
|
||||
const content = 'Test chat content';
|
||||
const options: ChatOptions = {
|
||||
// Provide mock values for the options
|
||||
parent: 'parent_value',
|
||||
reference: ['ref1', 'ref2'],
|
||||
header: ['header1', 'header2'],
|
||||
context: ['context1', 'context2'],
|
||||
};
|
||||
const mockResponse = {
|
||||
exitCode: 0,
|
||||
stdout: 'User: Test user\nDate: 2022-01-01\nprompt-hash: 12345\nTest chat response',
|
||||
stderr: '',
|
||||
};
|
||||
const mockWorkspacePath = './';
|
||||
|
||||
spawnAsyncStub.resolves(mockResponse);
|
||||
workspaceFoldersFirstPathStub.returns(mockWorkspacePath);
|
||||
|
||||
const response = await devChat.chat(content, options, (data)=>{});
|
||||
|
||||
expect(response).to.have.property('prompt-hash', '12345');
|
||||
expect(response).to.have.property('user', 'Test user');
|
||||
expect(response).to.have.property('date', '2022-01-01');
|
||||
expect(response).to.have.property('response', 'Test chat response');
|
||||
expect(response).to.have.property('isError', false);
|
||||
expect(spawnAsyncStub.calledOnce).to.be.true;
|
||||
expect(workspaceFoldersFirstPathStub.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
// Add more test cases for the chat method here
|
||||
});
|
||||
|
||||
// ... other test cases
|
||||
});
|
64
test/toolwrapper/dtm.test.ts
Normal file
64
test/toolwrapper/dtm.test.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import sinon from 'sinon';
|
||||
import DtmWrapper from '../../src/toolwrapper/dtm';
|
||||
|
||||
|
||||
describe('DtmWrapper', () => {
|
||||
let dtmWrapper: DtmWrapper;
|
||||
let commitStub: sinon.SinonStub;
|
||||
let commitAllStub: sinon.SinonStub;
|
||||
|
||||
beforeEach(() => {
|
||||
dtmWrapper = new DtmWrapper();
|
||||
commitStub = sinon.stub(dtmWrapper, 'commit');
|
||||
commitAllStub = sinon.stub(dtmWrapper, 'commitall');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
commitStub.restore();
|
||||
commitAllStub.restore();
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('should return a DtmResponse object with status 0 when the commit is successful', async () => {
|
||||
const commitMsg = 'Test commit message';
|
||||
const mockResponse = {
|
||||
status: 0,
|
||||
message: 'Commit successful',
|
||||
log: 'Commit log',
|
||||
};
|
||||
|
||||
commitStub.resolves(mockResponse);
|
||||
|
||||
const response = await dtmWrapper.commit(commitMsg);
|
||||
|
||||
expect(response).to.have.property('status', 0);
|
||||
expect(response).to.have.property('message');
|
||||
expect(response).to.have.property('log');
|
||||
expect(commitStub.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
// Add more test cases for the commit method here
|
||||
});
|
||||
|
||||
describe('commitall', () => {
|
||||
it('should return a DtmResponse object with status 0 when the commit is successful', async () => {
|
||||
const commitMsg = 'Test commit message';
|
||||
const mockResponse = {
|
||||
status: 0,
|
||||
message: 'Commit all successful',
|
||||
log: 'Commit all log',
|
||||
};
|
||||
|
||||
commitAllStub.resolves(mockResponse);
|
||||
|
||||
const response = await dtmWrapper.commitall(commitMsg);
|
||||
|
||||
expect(response).to.have.property('status', 0);
|
||||
expect(response).to.have.property('message');
|
||||
expect(response).to.have.property('log');
|
||||
expect(commitAllStub.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
49
test/topic/loadTopics.test.ts
Normal file
49
test/topic/loadTopics.test.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import { LogEntry } from '../../src/toolwrapper/devchat';
|
||||
import { loadTopicList } from '../../src/topic/loadTopics';
|
||||
|
||||
describe('loadTopicList', () => {
|
||||
it('should create topic lists from chat logs', () => {
|
||||
const chatLogs: LogEntry[] = [
|
||||
{ hash: '1', parent: '', user: 'user1', date: '2022-01-01', request: 'request1', response: 'response1', context: []},
|
||||
{ hash: '2', parent: '1', user: 'user2', date: '2022-01-02', request: 'request2', response: 'response2', context: []},
|
||||
{ hash: '3', parent: '2', user: 'user3', date: '2022-01-03', request: 'request3', response: 'response3', context: []},
|
||||
{ hash: '4', parent: '', user: 'user4', date: '2022-01-04', request: 'request4', response: 'response4', context: []},
|
||||
{ hash: '5', parent: '4', user: 'user5', date: '2022-01-05', request: 'request5', response: 'response5', context: []},
|
||||
];
|
||||
|
||||
const expectedTopicLists = {
|
||||
'1': [
|
||||
{ hash: '1', parent: '', user: 'user1', date: '2022-01-01', request: 'request1', response: 'response1', context: []},
|
||||
{ hash: '2', parent: '1', user: 'user2', date: '2022-01-02', request: 'request2', response: 'response2', context: []},
|
||||
{ hash: '3', parent: '2', user: 'user3', date: '2022-01-03', request: 'request3', response: 'response3', context: []},
|
||||
],
|
||||
'4': [
|
||||
{ hash: '4', parent: '', user: 'user4', date: '2022-01-04', request: 'request4', response: 'response4', context: []},
|
||||
{ hash: '5', parent: '4', user: 'user5', date: '2022-01-05', request: 'request5', response: 'response5', context: []},
|
||||
],
|
||||
};
|
||||
|
||||
const topicLists = loadTopicList(chatLogs);
|
||||
expect(topicLists).to.deep.equal(expectedTopicLists);
|
||||
});
|
||||
|
||||
it('should handle empty chat logs', () => {
|
||||
const chatLogs: LogEntry[] = [];
|
||||
const expectedTopicLists = {};
|
||||
const topicLists = loadTopicList(chatLogs);
|
||||
expect(topicLists).to.deep.equal(expectedTopicLists);
|
||||
});
|
||||
|
||||
it('should handle chat logs with no root entries', () => {
|
||||
const chatLogs: LogEntry[] = [
|
||||
{ hash: '1', parent: '0', user: 'user1', date: '2022-01-01', request: 'request1', response: 'response1', context: []},
|
||||
{ hash: '2', parent: '1', user: 'user2', date: '2022-01-02', request: 'request2', response: 'response2', context: []},
|
||||
];
|
||||
|
||||
const expectedTopicLists = {};
|
||||
const topicLists = loadTopicList(chatLogs);
|
||||
expect(topicLists).to.deep.equal(expectedTopicLists);
|
||||
});
|
||||
});
|
97
test/topic/topicManager.test.ts
Normal file
97
test/topic/topicManager.test.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
|
||||
import { TopicManager } from '../../src/topic/topicManager';
|
||||
|
||||
describe('TopicManager', () => {
|
||||
let topicManager: TopicManager;
|
||||
|
||||
beforeEach(() => {
|
||||
topicManager = TopicManager.getInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset topics and currentTopicId after each test
|
||||
topicManager['_topics'] = {};
|
||||
topicManager.currentTopicId = undefined;
|
||||
});
|
||||
|
||||
it('getInstance should return a singleton instance', () => {
|
||||
const instance1 = TopicManager.getInstance();
|
||||
const instance2 = TopicManager.getInstance();
|
||||
expect(instance1).to.equal(instance2);
|
||||
});
|
||||
|
||||
it('createTopic should create a new topic', () => {
|
||||
const topic = topicManager.createTopic();
|
||||
expect(topic).to.be.not.undefined;
|
||||
expect(topic.topicId).to.be.not.undefined;
|
||||
expect(topicManager.getTopic(topic.topicId)).to.equal(topic);
|
||||
});
|
||||
|
||||
it('getTopicList should return a list of topics', () => {
|
||||
const topic1 = topicManager.createTopic();
|
||||
const topic2 = topicManager.createTopic();
|
||||
const topicList = topicManager.getTopicList();
|
||||
expect(topicList).to.include(topic1);
|
||||
expect(topicList).to.include(topic2);
|
||||
});
|
||||
|
||||
it('setCurrentTopic should set the current topic ID', () => {
|
||||
const topic = topicManager.createTopic();
|
||||
topicManager.setCurrentTopic(topic.topicId);
|
||||
expect(topicManager.currentTopicId).to.equal(topic.topicId);
|
||||
});
|
||||
|
||||
it('updateTopic should update the topic with the given properties', () => {
|
||||
const topic = topicManager.createTopic();
|
||||
const newMessageHash = 'new-message-hash';
|
||||
const messageDate = Date.now();
|
||||
const requestMessage = 'Request message';
|
||||
const responseMessage = 'Response message';
|
||||
|
||||
topicManager.updateTopic(topic.topicId, newMessageHash, messageDate, requestMessage, responseMessage);
|
||||
|
||||
const updatedTopic = topicManager.getTopic(topic.topicId);
|
||||
expect(updatedTopic!.name).to.equal(`${requestMessage} - ${responseMessage}`);
|
||||
expect(updatedTopic!.firstMessageHash).to.equal(newMessageHash);
|
||||
expect(updatedTopic!.lastMessageHash).to.equal(newMessageHash);
|
||||
expect(updatedTopic!.lastUpdated).to.equal(messageDate);
|
||||
});
|
||||
|
||||
it('updateTopic should not update the topic if the topic does not exist', () => {
|
||||
const nonExistentTopicId = 'non-existent-topic-id';
|
||||
const newMessageHash = 'new-message-hash';
|
||||
const messageDate = Date.now();
|
||||
const requestMessage = 'Request message';
|
||||
const responseMessage = 'Response message';
|
||||
|
||||
topicManager.updateTopic(nonExistentTopicId, newMessageHash, messageDate, requestMessage, responseMessage);
|
||||
|
||||
const nonExistentTopic = topicManager.getTopic(nonExistentTopicId);
|
||||
expect(nonExistentTopic).to.be.undefined;
|
||||
});
|
||||
|
||||
it('deleteTopic should delete the topic with the given ID', () => {
|
||||
const topic = topicManager.createTopic();
|
||||
topicManager.deleteTopic(topic.topicId);
|
||||
expect(topicManager.getTopic(topic.topicId)).to.be.undefined;
|
||||
});
|
||||
|
||||
it('deleteTopic should not throw an error if the topic does not exist', () => {
|
||||
const nonExistentTopicId = 'non-existent-topic-id';
|
||||
expect(() => {
|
||||
topicManager.deleteTopic(nonExistentTopicId);
|
||||
}).to.not.throw();
|
||||
});
|
||||
|
||||
it('deleteTopic should set the currentTopicId to undefined if the deleted topic was the current topic', () => {
|
||||
const topic = topicManager.createTopic();
|
||||
topicManager.setCurrentTopic(topic.topicId);
|
||||
topicManager.deleteTopic(topic.topicId);
|
||||
expect(topicManager.currentTopicId).to.be.undefined;
|
||||
});
|
||||
|
||||
|
||||
// Add more test cases for other methods in TopicManager
|
||||
});
|
91
test/util/apiKey.test.ts
Normal file
91
test/util/apiKey.test.ts
Normal file
@ -0,0 +1,91 @@
|
||||
// test/apiKey.test.ts
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { ApiKeyManager } from '../../src/util/apiKey';
|
||||
import { UiUtilWrapper } from '../../src/util/uiUtil';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('ApiKeyManager', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
delete process.env.OPENAI_API_BASE;
|
||||
});
|
||||
|
||||
describe('getApiKey', () => {
|
||||
it('should return the secret storage API key', async () => {
|
||||
sinon.stub(UiUtilWrapper, 'secretStorageGet').resolves('sk.secret');
|
||||
sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined);
|
||||
|
||||
const apiKey = await ApiKeyManager.getApiKey();
|
||||
expect(apiKey).to.equal('sk.secret');
|
||||
});
|
||||
|
||||
it('should return the configuration API key', async () => {
|
||||
sinon.stub(UiUtilWrapper, 'secretStorageGet').resolves(undefined);
|
||||
sinon.stub(UiUtilWrapper, 'getConfiguration').returns('sk.config');
|
||||
|
||||
const apiKey = await ApiKeyManager.getApiKey();
|
||||
expect(apiKey).to.equal('sk.config');
|
||||
});
|
||||
|
||||
it('should return the environment variable API key', async () => {
|
||||
sinon.stub(UiUtilWrapper, 'secretStorageGet').resolves(undefined);
|
||||
sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined);
|
||||
process.env.OPENAI_API_KEY = 'sk.env';
|
||||
|
||||
const apiKey = await ApiKeyManager.getApiKey();
|
||||
expect(apiKey).to.equal('sk.env');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEndPoint', () => {
|
||||
it('should return the configuration endpoint', () => {
|
||||
sinon.stub(UiUtilWrapper, 'getConfiguration').returns('https://config-endpoint.com');
|
||||
|
||||
const endPoint = ApiKeyManager.getEndPoint('sk.key');
|
||||
expect(endPoint).to.equal('https://config-endpoint.com');
|
||||
});
|
||||
|
||||
it('should return the environment variable endpoint', () => {
|
||||
sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined);
|
||||
process.env.OPENAI_API_BASE = 'https://env-endpoint.com';
|
||||
|
||||
const endPoint = ApiKeyManager.getEndPoint('sk.key');
|
||||
expect(endPoint).to.equal('https://env-endpoint.com');
|
||||
});
|
||||
|
||||
it('should return the default endpoint for DC keys', () => {
|
||||
sinon.stub(UiUtilWrapper, 'getConfiguration').returns(undefined);
|
||||
|
||||
const endPoint = ApiKeyManager.getEndPoint('DC.key');
|
||||
expect(endPoint).to.equal('https://xw4ymuy6qj.ap-southeast-1.awsapprunner.com/api/v1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getKeyType', () => {
|
||||
it('should return "sk" for sk keys', () => {
|
||||
const keyType = ApiKeyManager.getKeyType('sk.key');
|
||||
expect(keyType).to.equal('sk');
|
||||
});
|
||||
|
||||
it('should return "DC" for DC keys', () => {
|
||||
const keyType = ApiKeyManager.getKeyType('DC.key');
|
||||
expect(keyType).to.equal('DC');
|
||||
});
|
||||
|
||||
it('should return undefined for invalid keys', () => {
|
||||
const keyType = ApiKeyManager.getKeyType('invalid.key');
|
||||
expect(keyType).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeApiKeySecret', () => {
|
||||
it('should store the API key in secret storage', async () => {
|
||||
const storeSecretStub = sinon.stub(UiUtilWrapper, 'storeSecret').resolves();
|
||||
|
||||
await ApiKeyManager.writeApiKeySecret('sk.secret');
|
||||
expect(storeSecretStub.calledWith('devchat_OPENAI_API_KEY', 'sk.secret')).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
138
test/util/commonUtil.test.ts
Normal file
138
test/util/commonUtil.test.ts
Normal file
@ -0,0 +1,138 @@
|
||||
// test/commonUtil.test.ts
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
createTempSubdirectory,
|
||||
CommandRun,
|
||||
runCommandAndWriteOutput,
|
||||
runCommandStringAndWriteOutput,
|
||||
getLanguageIdByFileName,
|
||||
} from '../../src/util/commonUtil';
|
||||
import { UiUtilWrapper } from '../../src/util/uiUtil';
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('commonUtil', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('createTempSubdirectory', () => {
|
||||
it('should create a temporary subdirectory', () => {
|
||||
const tempDir = os.tmpdir();
|
||||
const subdir = 'test-subdir';
|
||||
const targetDir = createTempSubdirectory(subdir);
|
||||
|
||||
expect(targetDir.startsWith(path.join(tempDir, subdir))).to.be.true;
|
||||
expect(fs.existsSync(targetDir)).to.be.true;
|
||||
fs.rmdirSync(targetDir, { recursive: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('CommandRun', () => {
|
||||
it('should run a command and capture stdout and stderr', async () => {
|
||||
const command = 'echo';
|
||||
const args = ['hello', 'world'];
|
||||
const options = { shell: true };
|
||||
|
||||
const run = new CommandRun();
|
||||
const result = await run.spawnAsync(command, args, options, undefined, undefined, undefined, undefined);
|
||||
|
||||
expect(result.exitCode).to.equal(0);
|
||||
expect(result.stdout.trim()).to.equal('hello world');
|
||||
expect(result.stderr).to.equal('');
|
||||
});
|
||||
|
||||
it('should run a command and write output to a file', async () => {
|
||||
const command = 'echo';
|
||||
const args = ['hello', 'world'];
|
||||
const options = { shell: true };
|
||||
const outputFile = path.join(os.tmpdir(), 'test-output.txt');
|
||||
|
||||
const run = new CommandRun();
|
||||
const result = await run.spawnAsync(command, args, options, undefined, undefined, undefined, outputFile);
|
||||
|
||||
expect(result.exitCode).to.equal(0);
|
||||
expect(result.stdout.trim()).to.equal('hello world');
|
||||
expect(result.stderr).to.equal('');
|
||||
expect(fs.readFileSync(outputFile, 'utf-8').trim()).to.equal('hello world');
|
||||
fs.unlinkSync(outputFile);
|
||||
});
|
||||
|
||||
it('should handle command not found error and output the error message', async () => {
|
||||
const command = 'nonexistent-command';
|
||||
const args: string[] = [];
|
||||
const options = { shell: true };
|
||||
|
||||
const run = new CommandRun();
|
||||
|
||||
const result = await run.spawnAsync(
|
||||
command,
|
||||
args,
|
||||
options,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(result.exitCode).to.not.equal(0);
|
||||
expect(result.stderr).to.include(`${command}: command not found`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runCommandAndWriteOutput', () => {
|
||||
it('should run a command and write output to a file', async () => {
|
||||
const command = 'echo';
|
||||
const args: string[] = ['hello', 'world'];
|
||||
const outputFile = path.join(os.tmpdir(), 'test-output.txt');
|
||||
|
||||
await runCommandAndWriteOutput(command, args, outputFile);
|
||||
|
||||
expect(fs.readFileSync(outputFile, 'utf-8').trim()).to.equal('hello world');
|
||||
fs.unlinkSync(outputFile);
|
||||
});
|
||||
});
|
||||
describe('runCommandStringAndWriteOutput', () => {
|
||||
it('should run a command string and write output to a file', async () => {
|
||||
const commandString = 'echo hello world';
|
||||
const outputFile = path.join(os.tmpdir(), 'test-output.txt');
|
||||
|
||||
await runCommandStringAndWriteOutput(commandString, outputFile);
|
||||
|
||||
const fileContent = fs.readFileSync(outputFile, 'utf-8').trim();
|
||||
const parsedContent = JSON.parse(fileContent);
|
||||
|
||||
expect(parsedContent.command).to.equal(commandString);
|
||||
expect(parsedContent.content.trim()).to.equal('hello world');
|
||||
fs.unlinkSync(outputFile);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLanguageIdByFileName', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(UiUtilWrapper, 'languageId').callsFake(async (fileName: string) => {
|
||||
const languageIds: { [key: string]: string } = {
|
||||
'test.py': 'python',
|
||||
'test.js': 'javascript',
|
||||
'test.ts': 'typescript',
|
||||
};
|
||||
return languageIds[fileName];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('should return the correct language ID for a given file name', async () => {
|
||||
expect(await getLanguageIdByFileName('test.py')).to.equal('python');
|
||||
expect(await getLanguageIdByFileName('test.js')).to.equal('javascript');
|
||||
expect(await getLanguageIdByFileName('test.ts')).to.equal('typescript');
|
||||
expect(await getLanguageIdByFileName('test.unknown')).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
});
|
45
test/util/filePairManager.test.ts
Normal file
45
test/util/filePairManager.test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import { FilePairManager } from '../../src/util/diffFilePairs';
|
||||
|
||||
describe('FilePairManager', () => {
|
||||
let filePairManager: FilePairManager;
|
||||
|
||||
beforeEach(() => {
|
||||
filePairManager = FilePairManager.getInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear the filePairs map after each test
|
||||
(filePairManager as any).filePairs.clear();
|
||||
});
|
||||
|
||||
it('add file pair', () => {
|
||||
const file1 = 'file1.txt';
|
||||
const file2 = 'file2.txt';
|
||||
filePairManager.addFilePair(file1, file2);
|
||||
expect(filePairManager.findPair(file1)).to.deep.equal([file1, file2]);
|
||||
expect(filePairManager.findPair(file2)).to.deep.equal([file1, file2]);
|
||||
});
|
||||
|
||||
it('find pair', () => {
|
||||
const file1 = 'file1.txt';
|
||||
const file2 = 'file2.txt';
|
||||
const file3 = 'file3.txt';
|
||||
const file4 = 'file4.txt';
|
||||
filePairManager.addFilePair(file1, file2);
|
||||
filePairManager.addFilePair(file3, file4);
|
||||
expect(filePairManager.findPair(file1)).to.deep.equal([file1, file2]);
|
||||
expect(filePairManager.findPair(file2)).to.deep.equal([file1, file2]);
|
||||
expect(filePairManager.findPair(file3)).to.deep.equal([file3, file4]);
|
||||
expect(filePairManager.findPair(file4)).to.deep.equal([file3, file4]);
|
||||
});
|
||||
|
||||
it('find non-existent pair', () => {
|
||||
const file1 = 'file1.txt';
|
||||
const file2 = 'file2.txt';
|
||||
const file3 = 'file3.txt';
|
||||
filePairManager.addFilePair(file1, file2);
|
||||
expect(filePairManager.findPair(file3)).to.be.undefined;
|
||||
});
|
||||
});
|
64
test/util/logger.test.ts
Normal file
64
test/util/logger.test.ts
Normal file
@ -0,0 +1,64 @@
|
||||
// test/util/logger.test.ts
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import { logger, LogChannel } from '../../src/util/logger';
|
||||
|
||||
class MockLogChannel implements LogChannel {
|
||||
logs: string[] = [];
|
||||
|
||||
info(message: string, ...args: any[]): void {
|
||||
this.logs.push(`[INFO] ${message} ${args.join(' ')}`);
|
||||
}
|
||||
|
||||
warn(message: string, ...args: any[]): void {
|
||||
this.logs.push(`[WARN] ${message} ${args.join(' ')}`);
|
||||
}
|
||||
|
||||
error(message: string | Error, ...args: any[]): void {
|
||||
this.logs.push(`[ERROR] ${message} ${args.join(' ')}`);
|
||||
}
|
||||
|
||||
debug(message: string, ...args: any[]): void {
|
||||
this.logs.push(`[DEBUG] ${message} ${args.join(' ')}`);
|
||||
}
|
||||
|
||||
show(): void {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
describe('logger', () => {
|
||||
it('should initialize the logger and create a channel', () => {
|
||||
// Arrange
|
||||
const mockChannel = new MockLogChannel();
|
||||
|
||||
// Act
|
||||
logger.init(mockChannel);
|
||||
|
||||
// Assert
|
||||
const channel = logger.channel();
|
||||
expect(channel).to.not.be.undefined;
|
||||
expect(channel).to.equal(mockChannel);
|
||||
});
|
||||
|
||||
it('should log messages using the initialized channel', () => {
|
||||
// Arrange
|
||||
const mockChannel = new MockLogChannel();
|
||||
logger.init(mockChannel);
|
||||
|
||||
// Act
|
||||
logger.channel()?.info('Test info message');
|
||||
logger.channel()?.warn('Test warn message');
|
||||
logger.channel()?.error('Test error message');
|
||||
logger.channel()?.debug('Test debug message');
|
||||
|
||||
// Assert
|
||||
expect(mockChannel.logs).to.deep.equal([
|
||||
'[INFO] Test info message ',
|
||||
'[WARN] Test warn message ',
|
||||
'[ERROR] Test error message ',
|
||||
'[DEBUG] Test debug message ',
|
||||
]);
|
||||
});
|
||||
});
|
45
test/util/messageHistory.test.ts
Normal file
45
test/util/messageHistory.test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import { MessageHistory } from '../../src/util/messageHistory';
|
||||
|
||||
describe('MessageHistory', () => {
|
||||
let messageHistory: MessageHistory;
|
||||
|
||||
beforeEach(() => {
|
||||
messageHistory = new MessageHistory();
|
||||
});
|
||||
|
||||
it('add message', () => {
|
||||
const message = { hash: '123', content: 'Hello' };
|
||||
messageHistory.add(message);
|
||||
expect(messageHistory.find('123')).to.deep.equal(message);
|
||||
});
|
||||
|
||||
it('find message by hash', () => {
|
||||
const message1 = { hash: '123', content: 'Hello' };
|
||||
const message2 = { hash: '456', content: 'World' };
|
||||
messageHistory.add(message1);
|
||||
messageHistory.add(message2);
|
||||
expect(messageHistory.find('123')).to.deep.equal(message1);
|
||||
expect(messageHistory.find('456')).to.deep.equal(message2);
|
||||
});
|
||||
|
||||
it('find last message', () => {
|
||||
const message1 = { hash: '123', content: 'Hello' };
|
||||
const message2 = { hash: '456', content: 'World' };
|
||||
messageHistory.add(message1);
|
||||
messageHistory.add(message2);
|
||||
expect(messageHistory.findLast()).to.deep.equal(message2);
|
||||
});
|
||||
|
||||
it('clear history', () => {
|
||||
const message1 = { hash: '123', content: 'Hello' };
|
||||
const message2 = { hash: '456', content: 'World' };
|
||||
messageHistory.add(message1);
|
||||
messageHistory.add(message2);
|
||||
messageHistory.clear();
|
||||
expect(messageHistory.find('123')).to.be.undefined;
|
||||
expect(messageHistory.find('456')).to.be.undefined;
|
||||
expect(messageHistory.findLast()).to.be.null;
|
||||
});
|
||||
});
|
13
test/util/utils.test.ts
Normal file
13
test/util/utils.test.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
|
||||
describe('yourFunction', () => {
|
||||
it('should return the correct result for input 1', () => {
|
||||
const input = 1;
|
||||
const expectedResult = 'expectedResult';
|
||||
const result = 'expectedResult';
|
||||
expect(result).to.equal(expectedResult);
|
||||
});
|
||||
|
||||
// Add more test cases here
|
||||
});
|
@ -13,5 +13,6 @@
|
||||
"strict": true,
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
},
|
||||
"exclude": ["test"]
|
||||
}
|
9
tsconfig.test.json
Normal file
9
tsconfig.test.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "out-test",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user