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

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

@ -10,9 +10,28 @@
"main": "./out/extension.js", "main": "./out/extension.js",
"activationEvents": [ "activationEvents": [
"*", "*",
"onCommand:ai-chat.openWebview" "onCommand:ai-chat.openWebview",
"onView:ai-chat-sidebar-view"
], ],
"contributes": { "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": { "menus": {
"editor/context": [ "editor/context": [
{ {

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

Loading…
Cancel
Save