feat(extension): 将 AI Chat功能迁移到辅助侧边栏视图

master
钟良源 6 months ago
parent 135b0a8afe
commit c8c5413519

@ -10,9 +10,28 @@
"main": "./out/extension.js",
"activationEvents": [
"*",
"onCommand:ai-chat.openWebview"
"onCommand:ai-chat.openWebview",
"onView:ai-chat-sidebar-view"
],
"contributes": {
"viewsContainers": {
"secondarySidebar": [
{
"id": "ai-chat-sidebar",
"title": "AI Chat",
"icon": "media/icon.png"
}
]
},
"views": {
"ai-chat-sidebar": [
{
"id": "ai-chat-sidebar-view",
"name": "AI Chat",
"type": "webview"
}
]
},
"menus": {
"editor/context": [
{

@ -17,16 +17,20 @@ let workspaceChanges: {
export function activate(context: vscode.ExtensionContext) {
console.log('Extension activated');
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
statusBarItem.text = "$(comment) Chat";
statusBarItem.tooltip = "Open AI Chat";
statusBarItem.command = "ai-chat.openWebview";
statusBarItem.show();
// 注册辅助侧边栏视图提供商
const provider = new AISidebarViewProvider(context.extensionPath, context);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
'ai-chat-sidebar-view',
provider,
{ webviewOptions: { retainContextWhenHidden: true } }
)
);
// 注册打开 Webview 的命令
// 注册打开 Webview 的命令(保持向后兼容)
const openWebviewCommand = vscode.commands.registerCommand("ai-chat.openWebview", () => {
openWebview(context.extensionPath, context.globalState, context);
// 聚焦到辅助侧边栏视图
vscode.commands.executeCommand('workbench.action.focusAuxiliaryBar');
});
// 注册右键菜单命令:添加选中内容到对话
@ -46,147 +50,43 @@ export function activate(context: vscode.ExtensionContext) {
const selectedText = editor.document.getText(selection);
// 检查 panel 是否存在
if (panel && panel.webview) {
panel.webview.postMessage({
command: 'addToInput',
role: 'user',
content: selectedText
});
} else {
vscode.window.showWarningMessage("请先打开对话窗口");
}
});
// 注册选择文件命令
const selectFileCommand = vscode.commands.registerCommand('ai-chat.selectFileAsContext', async () => {
const uris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false, // 单选
title: "选择一个文件作为上下文"
});
if (uris && uris.length > 0) {
const selectedFileUri = uris[0];
const fileContent = await vscode.workspace.fs.readFile(selectedFileUri);
const decodedContent = new TextDecoder("utf-8").decode(fileContent);
// 发送文件内容到 Webview
if (panel && panel.webview) {
panel.webview.postMessage({
command: 'setContextFile',
fileName: selectedFileUri.fsPath,
fileContent: decodedContent
});
}
}
});
// 注册获取工作区文件列表的命令
const getFileListCommand = vscode.commands.registerCommand('ai-chat.getFileList', async () => {
if (panel && panel.webview) {
try {
// 获取工作区根路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
panel.webview.postMessage({
command: 'fileList',
files: [],
error: '未找到工作区'
});
return;
}
const rootPath = workspaceFolders[0].uri;
// 查找所有文件
const fileUris = await vscode.workspace.findFiles('**/*', '**/node_modules/**');
// 转换为相对路径和基本信息
const files = fileUris.map(uri => {
const relativePath = vscode.workspace.asRelativePath(uri);
const isDirectory = uri.path.endsWith('/');
return {
path: uri.fsPath,
relativePath: relativePath,
name: path.basename(uri.fsPath),
isDirectory: isDirectory
};
});
panel.webview.postMessage({
command: 'fileList',
files: files
});
} catch (error) {
panel.webview.postMessage({
command: 'fileList',
files: [],
error: error.message
});
}
}
// 发送选中的代码到侧边栏视图
provider.sendSelectedCode(selectedText);
});
// 添加到 subscriptions
context.subscriptions.push(statusBarItem, openWebviewCommand, addToChatCommand,selectFileCommand,getFileListCommand);
context.subscriptions.push(openWebviewCommand, addToChatCommand);
}
function openWebview(
extensionPath: string,
globalState: vscode.Memento,
context: vscode.ExtensionContext // 添加 context 参数
) {
// 如果面板已经存在,直接显示它而不是创建新的
if (panel) {
panel.reveal(vscode.ViewColumn.Beside);
// 发送当前状态到 WebView
if (panel.webview) {
panel.webview.postMessage({
command: 'restoreState',
history: currentSessionHistory,
workspaceChanges: workspaceChanges
});
}
return;
}
class AISidebarViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'ai-chat-sidebar-view';
private _view?: vscode.WebviewView;
private _extensionPath: string;
private _context: vscode.ExtensionContext;
panel = vscode.window.createWebviewPanel(
'aiChatWebview',
'AI Chat',
vscode.ViewColumn.Beside,
{
enableScripts: true,
localResourceRoots: [vscode.Uri.file(path.join(extensionPath, 'media', 'webview'))]
}
);
const styleUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'style.css'))
);
const scriptUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'script.js'))
);
const highlightScriptUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'highlight.min.js'))
);
constructor(extensionPath: string, context: vscode.ExtensionContext) {
this._extensionPath = extensionPath;
this._context = context;
}
const highlightStyleUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(extensionPath, 'media', 'webview', 'styles', 'atom-one-dark.css'))
);
const markedScriptUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(context.extensionPath, 'media', 'webview', 'marked.min.js'))
);
public resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken,
) {
this._view = webviewView;
webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview'))
]
};
panel.webview.html = getWebviewContent(styleUri, scriptUri,highlightScriptUri,highlightStyleUri,markedScriptUri);
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
panel.webview.onDidReceiveMessage(
async (message) => {
webviewView.webview.onDidReceiveMessage(async (message) => {
switch (message.command) {
case 'ask':
const question = message.text;
@ -195,10 +95,10 @@ function openWebview(
currentSessionHistory = [...history, { role: 'user', content: question }];
panel.webview.postMessage({ command: 'addMessage', role: 'user', content: question });
this._postMessage({ command: 'addMessage', role: 'user', content: question });
try {
const response = await callQwenAPI(question, history, fileContent, context);
const response = await callQwenAPI(question, history, fileContent, this._context);
console.log("response:", response);
const aiMessage = response.choices[0]?.message?.content || '未获取到回复';
@ -215,28 +115,26 @@ function openWebview(
};
// 通知 WebView 更新工作区文件列表
if (panel && panel.webview) {
panel.webview.postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
}
this._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
}
// 更新当前会话历史
currentSessionHistory = [...currentSessionHistory, { role: 'assistant', content: aiMessage }];
panel.webview.postMessage({
this._postMessage({
command: 'addMessage',
role: 'ai',
content: aiMessage,
codeDiff: codeDiff // 发送代码差异信息
});
} catch (error) {
panel.webview.postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message });
this._postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message });
}
panel.webview.postMessage({ command: 'hideLoading' });
this._postMessage({ command: 'hideLoading' });
break;
case 'selectContextFile':
// 执行文件选择逻辑
@ -252,7 +150,7 @@ function openWebview(
const fileContent = await vscode.workspace.fs.readFile(selectedFileUri);
const decodedContent = new TextDecoder("utf-8").decode(fileContent);
panel.webview.postMessage({
this._postMessage({
command: 'setContextFile',
fileName: selectedFileUri.fsPath,
fileContent: decodedContent
@ -275,7 +173,7 @@ function openWebview(
};
});
panel.webview.postMessage({
this._postMessage({
command: 'fileList',
files: files
});
@ -289,7 +187,7 @@ function openWebview(
const fileContent = await vscode.workspace.fs.readFile(fileUri);
const decodedContent = new TextDecoder("utf-8").decode(fileContent);
panel.webview.postMessage({
this._postMessage({
command: 'setContextFile',
fileName: fileUri.fsPath,
fileContent: decodedContent
@ -347,12 +245,10 @@ function openWebview(
}
// 通知 WebView 更新工作区文件列表
if (panel && panel.webview) {
panel.webview.postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
}
this._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
vscode.window.showInformationMessage('代码变更已应用');
} catch (error) {
@ -370,12 +266,10 @@ function openWebview(
}
// 通知 WebView 更新工作区文件列表
if (panel && panel.webview) {
panel.webview.postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
}
this._postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
vscode.window.showInformationMessage('代码变更已拒绝');
} catch (error) {
@ -406,7 +300,7 @@ function openWebview(
// 如果文件未打开,则在新窗口打开
if (!fileAlreadyOpen) {
await vscode.window.showTextDocument(fileUri, {
viewColumn: vscode.ViewColumn.One // 在新窗口打开文件
viewColumn: vscode.ViewColumn.One // 在第一列打开文件
});
}
} catch (error) {
@ -415,24 +309,77 @@ function openWebview(
break;
case 'getCodeChanges':
// 获取代码变更
if (panel && panel.webview) {
panel.webview.postMessage({
command: 'codeChanges',
changes: workspaceChanges
this._postMessage({
command: 'codeChanges',
changes: workspaceChanges
});
break;
case 'restoreState':
// 恢复面板状态
currentSessionHistory = message.history || [];
// 恢复显示历史消息
currentSessionHistory.forEach(msg => {
this._postMessage({
command: 'addMessage',
role: msg.role,
content: msg.content,
codeDiff: null
});
});
// 恢复工作区变更
if (message.workspaceChanges) {
this._postMessage({
command: 'updateWorkspaceFiles',
files: message.workspaceChanges
});
}
break;
}
},
undefined,
context.subscriptions
);
});
}
public sendSelectedCode(code: string) {
if (this._view) {
this._view.show?.(true);
this._postMessage({
command: 'addToInput',
role: 'user',
content: code
});
}
}
private _getHtmlForWebview(webview: vscode.Webview) {
const styleUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'style.css'))
);
// // 添加 dispose 监听器
panel.onDidDispose(() => {
// 不再清空会话历史,让用户在重新打开面板时保留上下文
panel = undefined;
}, null, context.subscriptions);
const scriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'script.js'))
);
const highlightScriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'highlight.min.js'))
);
const highlightStyleUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'styles', 'atom-one-dark.css'))
);
const markedScriptUri = webview.asWebviewUri(
vscode.Uri.file(path.join(this._extensionPath, 'media', 'webview', 'marked.min.js'))
);
return getWebviewContent(styleUri, scriptUri, highlightScriptUri, highlightStyleUri, markedScriptUri);
}
private _postMessage(message: any) {
if (this._view) {
this._view.webview.postMessage(message);
}
}
}
async function callQwenAPI(
@ -625,4 +572,4 @@ function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlight
<script src="${markedScriptUri}"></script>
</body>
</html>`;
}
}

Loading…
Cancel
Save