Separate VSCode from core logic for ISSUE #125
- Extract VSCode-related code from core logic. - Improve modularity for easier unit testing.
This commit is contained in:
parent
562a125902
commit
82faf14eb6
@ -9,6 +9,8 @@
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "warn",
|
||||
"no-unused-expressions": "warn",
|
||||
"@typescript-eslint/naming-convention": "warn",
|
||||
"@typescript-eslint/semi": "warn",
|
||||
"curly": "warn",
|
||||
|
@ -4,6 +4,7 @@ import { createTempSubdirectory, runCommandStringAndWriteOutput } from '../util/
|
||||
import { logger } from '../util/logger';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
|
||||
export const customCommandContext: ChatContext = {
|
||||
name: '<custom command>',
|
||||
description: 'custorm command',
|
||||
@ -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,141 +1,64 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { sendFileSelectMessage, sendCodeSelectMessage } from './util';
|
||||
import { logger } from '../util/logger';
|
||||
import * as childProcess from 'child_process';
|
||||
import ExtensionContextHolder from '../util/extensionContext';
|
||||
import { TopicManager, Topic } from '../topic/topicManager';
|
||||
import { TopicManager } from '../topic/topicManager';
|
||||
import { TopicTreeDataProvider, TopicTreeItem } from '../panel/topicView';
|
||||
import { FilePairManager } from '../util/diffFilePairs';
|
||||
|
||||
|
||||
import * as process from 'process';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
|
||||
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() {
|
||||
let openaiApiKey = await UiUtilWrapper.secretStorageGet("devchat_OPENAI_API_KEY");
|
||||
if (!openaiApiKey) {
|
||||
openaiApiKey = UiUtilWrapper.getConfiguration('DevChat', 'API_KEY');
|
||||
}
|
||||
if (!openaiApiKey) {
|
||||
openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
}
|
||||
if (!openaiApiKey) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const disposableAskFileChinese = vscode.commands.registerCommand('devchat.askForFile_chinese', async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
if (!await ensureChatPanel(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendFileSelectMessage(ExtensionContextHolder.provider?.view()!, editor.document.fileName);
|
||||
}
|
||||
});
|
||||
context.subscriptions.push(disposableAskFileChinese);
|
||||
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));
|
||||
}
|
||||
|
||||
export function registerApiKeySettingCommand(context: vscode.ExtensionContext) {
|
||||
@ -160,11 +83,27 @@ export function registerStatusBarItemClickCommand(context: vscode.ExtensionConte
|
||||
);
|
||||
}
|
||||
|
||||
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', (item: TopicTreeItem) => {
|
||||
TopicManager.getInstance().deleteTopic(item.id);
|
||||
})
|
||||
vscode.commands.registerCommand('devchat-topicview.deleteTopic', topicDeleteCallback)
|
||||
);
|
||||
}
|
||||
|
||||
@ -182,7 +121,7 @@ export function regDeleteSelectTopicCommand(context: vscode.ExtensionContext) {
|
||||
vscode.commands.registerCommand('devchat-topicview.deleteSelectedTopic', () => {
|
||||
const selectedItem = TopicTreeDataProvider.getInstance().selectedItem;
|
||||
if (selectedItem) {
|
||||
TopicManager.getInstance().deleteTopic(selectedItem.id);
|
||||
topicDeleteCallback(selectedItem);
|
||||
} else {
|
||||
vscode.window.showErrorMessage('No item selected');
|
||||
}
|
||||
@ -201,7 +140,7 @@ export function regSelectTopicCommand(context: vscode.ExtensionContext) {
|
||||
|
||||
export function regReloadTopicCommand(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('devchat-topicview.reloadTopic', async (item: TopicTreeItem) => {
|
||||
vscode.commands.registerCommand('devchat-topicview.reloadTopic', async () => {
|
||||
TopicManager.getInstance().loadTopics();
|
||||
})
|
||||
);
|
||||
@ -209,7 +148,7 @@ export function regReloadTopicCommand(context: vscode.ExtensionContext) {
|
||||
|
||||
export function regApplyDiffResultCommand(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('devchat.applyDiffResult', async (data) => {
|
||||
vscode.commands.registerCommand('devchat.applyDiffResult', async () => {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
const fileName = activeEditor!.document.fileName;
|
||||
|
||||
@ -236,8 +175,8 @@ export function regApplyDiffResultCommand(context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
export {
|
||||
registerOpenChatPanelCommand,
|
||||
registerAddContextCommand,
|
||||
registerAskForCodeCommand,
|
||||
registerAskForFileCommand,
|
||||
registerOpenChatPanelCommand,
|
||||
registerAddContextCommand,
|
||||
registerAskForCodeCommand,
|
||||
registerAskForFileCommand,
|
||||
};
|
||||
|
45
src/contributes/commandsBase.ts
Normal file
45
src/contributes/commandsBase.ts
Normal file
@ -0,0 +1,45 @@
|
||||
|
||||
import * as childProcess from 'child_process';
|
||||
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
|
||||
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() {
|
||||
let openaiApiKey = await UiUtilWrapper.secretStorageGet("devchat_OPENAI_API_KEY");
|
||||
if (!openaiApiKey) {
|
||||
openaiApiKey = UiUtilWrapper.getConfiguration('DevChat', 'API_KEY');
|
||||
}
|
||||
if (!openaiApiKey) {
|
||||
openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
}
|
||||
if (!openaiApiKey) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
208
src/extension.ts
208
src/extension.ts
@ -1,21 +1,18 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import {
|
||||
checkOpenaiApiKey,
|
||||
checkDevChatDependency,
|
||||
checkDependencyPackage,
|
||||
registerOpenChatPanelCommand,
|
||||
registerAddContextCommand,
|
||||
registerAskForCodeCommand,
|
||||
registerAskForFileCommand,
|
||||
registerApiKeySettingCommand,
|
||||
registerStatusBarItemClickCommand,
|
||||
regTopicDeleteCommand,
|
||||
regAddTopicCommand,
|
||||
regDeleteSelectTopicCommand,
|
||||
regSelectTopicCommand,
|
||||
regReloadTopicCommand,
|
||||
regApplyDiffResultCommand,
|
||||
registerStatusBarItemClickCommand,
|
||||
} from './contributes/commands';
|
||||
import { regLanguageContext } from './contributes/context';
|
||||
import { regDevChatView, regTopicView } from './contributes/views';
|
||||
@ -24,7 +21,7 @@ import ExtensionContextHolder from './util/extensionContext';
|
||||
import { logger } from './util/logger';
|
||||
import { LoggerChannelVscode } from './util/logger_vscode';
|
||||
import { createStatusBarItem } from './panel/statusBarView';
|
||||
import { UiUtilWrapper } from './util/uiUtil';
|
||||
import { UiUtilWrapper, UiUtilVscode } from './util/uiUtil';
|
||||
|
||||
|
||||
function activate(context: vscode.ExtensionContext) {
|
||||
@ -43,200 +40,15 @@ function activate(context: vscode.ExtensionContext) {
|
||||
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;
|
||||
|
@ -1,141 +1,19 @@
|
||||
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);
|
||||
}
|
||||
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;
|
||||
const historyMessage = historyMessagesBase();
|
||||
MessageHandler.sendMessage(panel, historyMessage);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
134
src/handler/historyMessagesBase.ts
Normal file
134
src/handler/historyMessagesBase.ts
Normal file
@ -0,0 +1,134 @@
|
||||
|
||||
|
||||
import { checkOpenaiApiKey } from '../contributes/commandsBase';
|
||||
import { TopicManager } from '../topic/topicManager';
|
||||
import { LogEntry } from '../toolwrapper/devchat';
|
||||
import messageHistory from '../util/messageHistory';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
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 (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 loadTopicHistoryLogs() : Promise<Array<LogEntry>> {
|
||||
const topicId = TopicManager.getInstance().currentTopicId;
|
||||
let logEntriesFlat: Array<LogEntry> = [];
|
||||
if (topicId) {
|
||||
logEntriesFlat = await TopicManager.getInstance().getTopicHistory(topicId);
|
||||
}
|
||||
|
||||
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 isApiKeyReady = await checkOpenaiApiKey();
|
||||
isApiSet = true;
|
||||
if (!isApiKeyReady) {
|
||||
const startMessage = [ apiKeyMissedMessage() ];
|
||||
isApiSet = false;
|
||||
|
||||
return {
|
||||
command: 'loadHistoryMessages',
|
||||
entries: startMessage,
|
||||
} as LoadHistoryMessages;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function historyMessagesBase(): Promise<LoadHistoryMessages> {
|
||||
const logEntriesFlat = await loadTopicHistoryLogs();
|
||||
updateCurrentMessageHistory(logEntriesFlat);
|
||||
|
||||
const apiKeyMessage = await apiKeyInvalidMessage();
|
||||
if (apiKeyMessage) {
|
||||
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;
|
||||
UiUtilWrapper.storeSecret("devchat_OPENAI_API_KEY", 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 { checkOpenaiApiKey } from '../contributes/commandsBase';
|
||||
|
||||
|
||||
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') {
|
||||
|
@ -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);
|
||||
});
|
||||
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
|
||||
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;
|
||||
|
||||
|
||||
// 将解析消息的部分提取到一个单独的函数中
|
||||
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;
|
||||
}
|
||||
|
||||
// 将处理父哈希的部分提取到一个单独的函数中
|
||||
function getParentHash(message: 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;
|
||||
}
|
||||
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.indexOf('Exit code: undefined') >= 0) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (chatResponse.isError) {
|
||||
responseText = partialDataText + 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();
|
||||
}
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -1,20 +1,8 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { checkOpenAiAPIKey, checkDevChatDependency } from '../contributes/commands';
|
||||
import { logger } from '../util/logger';
|
||||
import { TopicManager } from '../topic/topicManager';
|
||||
import { dependencyCheck } from './statusBarViewBase';
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function createStatusBarItem(context: vscode.ExtensionContext): vscode.StatusBarItem {
|
||||
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
||||
|
||||
@ -24,70 +12,16 @@ export function createStatusBarItem(context: vscode.ExtensionContext): vscode.St
|
||||
statusBarItem.command = '';
|
||||
|
||||
// add a timer to update the status bar item
|
||||
let devchatStatus = '';
|
||||
let apiKeyStatus = '';
|
||||
|
||||
const extensionVersion = getExtensionVersion(context);
|
||||
const secretStorage: vscode.SecretStorage = context.secrets;
|
||||
|
||||
setInterval(async () => {
|
||||
const versionOld = await secretStorage.get("devchat_version_old");
|
||||
const versionNew = extensionVersion;
|
||||
const versionChanged = versionOld !== versionNew;
|
||||
secretStorage.store("devchat_version_old", 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 = 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
|
||||
const terminal = vscode.window.createTerminal("DevChat Install");
|
||||
terminal.sendText(`python ${context.extensionUri.fsPath + "/tools/install.py"}`);
|
||||
terminal.show();
|
||||
devchatStatus = 'waiting install devchat';
|
||||
}
|
||||
|
||||
setInterval(async () => {
|
||||
const [devchatStatus, apiKeyStatus] = await dependencyCheck();
|
||||
if (devchatStatus !== 'ready') {
|
||||
statusBarItem.text = `$(warning)DevChat`;
|
||||
statusBarItem.tooltip = `${devchatStatus}`;
|
||||
statusBarItem.command = '';
|
||||
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}`;
|
||||
|
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 { checkOpenaiApiKey } from "../contributes/commandsBase";
|
||||
|
||||
|
||||
|
||||
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 checkOpenaiApiKey();
|
||||
if (bOk) {
|
||||
apiKeyStatus = 'ready';
|
||||
} else {
|
||||
apiKeyStatus = 'please set api key';
|
||||
}
|
||||
}
|
||||
|
||||
return [devchatStatus, apiKeyStatus];
|
||||
}
|
@ -84,7 +84,7 @@ class DevChat {
|
||||
return args;
|
||||
}
|
||||
|
||||
async getOpenAiApiKey(): Promise<string | undefined> {
|
||||
async getOpenaiApiKey(): Promise<string | undefined> {
|
||||
let openaiApiKey = await UiUtilWrapper.secretStorageGet("devchat_OPENAI_API_KEY");
|
||||
if (!openaiApiKey) {
|
||||
openaiApiKey = UiUtilWrapper.getConfiguration('DevChat', 'API_KEY');
|
||||
@ -153,8 +153,9 @@ class DevChat {
|
||||
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');
|
||||
|
||||
if (UiUtilWrapper.getConfiguration('DevChat', 'API_ENDPOINT')) {
|
||||
openAiApiBase = UiUtilWrapper.getConfiguration('DevChat', 'API_ENDPOINT');
|
||||
}
|
||||
|
||||
const openAiApiBaseObject = openAiApiBase ? { OPENAI_API_BASE: openAiApiBase } : {};
|
||||
|
@ -4,6 +4,7 @@ import * as fs from 'fs';
|
||||
|
||||
import { logger } from "../util/logger";
|
||||
import { CommandRun } from "../util/commonUtil";
|
||||
import { UiUtilWrapper } from "../util/uiUtil";
|
||||
|
||||
interface DtmResponse {
|
||||
status: number;
|
||||
|
@ -5,6 +5,7 @@ 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;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import ExtensionContextHolder from './extensionContext';
|
||||
|
||||
export interface UiUtil {
|
||||
languageId(uri: string): Promise<string>;
|
||||
@ -6,6 +7,9 @@ export interface UiUtil {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -31,10 +35,19 @@ export class UiUtilVscode implements UiUtil {
|
||||
vscode.workspace.fs.writeFile(vscode.Uri.file(uri), Buffer.from(content));
|
||||
}
|
||||
public async showInputBox(option: object): Promise<string | undefined> {
|
||||
return vscode.window.showInputBox({
|
||||
prompt: 'Input your custom command',
|
||||
placeHolder: 'for example: ls -l'
|
||||
});
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,4 +75,14 @@ export class UiUtilWrapper {
|
||||
public static async showInputBox(option: object): Promise<string | undefined> {
|
||||
return this._uiUtil?.showInputBox(option);
|
||||
}
|
||||
}
|
||||
public static async storeSecret(key: string, value: string): Promise<void> {
|
||||
return 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,9 @@
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
},
|
||||
"exclude": ["test"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user