feat(extension): 增加工作区变更管理和代码差异对比功能- 新增工作区变更列表,显示文件状态和变更内容

- 实现代码差异对比,支持接受或拒绝变更
- 优化上下文选择界面和消息展示样式- 添加加载指示和错误处理
master
钟良源 6 months ago
parent 3671cd1b93
commit 135b0a8afe

@ -1,28 +1,73 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>AI Chat</title> <title>AI Chat</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="${styleUri}" /> <link rel="stylesheet" href="${styleUri}" />
<link href="${highlightStyleUri}" rel="stylesheet" />
</head> </head>
<body> <body>
<div class="chat-container"> <div class="chat-container">
<!--对话区-->
<div id="chat-box"></div> <div id="chat-box"></div>
<!-- 添加上下文区域 --> <!--文件选择-->
<!-- <div class="context-area"> <div class="context-container">
<button id="add-context-btn">+ 添加上下文</button> <button id="select-context-btn" class="select-context-btn" title="选择上下文文件">+</button>
<div class="context-options">
<span>智能问答</span> <div id="context-placeholder" class="context-placeholder">
<select id="mode-select"> <span class="placeholder-text">添加上下文</span>
<option value="auto">Auto</option> </div>
<option value="manual">Manual</option>
</select> <div id="context-tab" class="context-tab hidden">
<label for="tool-checkbox"><input type="checkbox" id="tool-checkbox"> 工具</label> <span id="context-file-name" class="file-name">添加上下文</span>
</div> <button id="close-context-btn" class="close-btn" title="清除上下文">×</button>
<div class="keybinds">Shift+Enter 换行/Enter 发送 →</div> </div>
</div> --> </div>
<!-- 工作区文件列表 -->
<div class="workspace-container">
<h3>工作区变更</h3>
<div id="workspace-files" class="workspace-files">
<div class="no-changes">暂无文件变更</div>
</div>
</div>
<!-- 文件浏览器模态框 -->
<div id="file-browser-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3>选择上下文文件</h3>
<span id="close-modal" class="close-modal">&times;</span>
</div>
<div class="modal-body">
<div class="file-search">
<input type="text" id="file-search-input" placeholder="搜索文件..." />
</div>
<div id="file-list" class="file-list">
<div class="loading">加载中...</div>
</div>
</div>
</div>
</div>
<!-- 代码差异模态框 -->
<div id="diff-modal" class="modal hidden">
<div class="modal-content diff-modal-content">
<div class="modal-header">
<h3>代码差异对比</h3>
<span id="close-diff-modal" class="close-modal">&times;</span>
</div>
<div class="modal-body">
<div id="diff-content" class="diff-content"></div>
<div class="diff-actions">
<button id="accept-changes-btn" class="diff-action-btn accept">接受变更</button>
<button id="reject-changes-btn" class="diff-action-btn reject">拒绝变更</button>
</div>
</div>
</div>
</div>
<!-- 聊天输入表单 --> <!-- 聊天输入表单 -->
<form id="chat-form"> <form id="chat-form">
@ -32,5 +77,7 @@
<div id="loading" class="hidden">AI 正在思考...</div> <div id="loading" class="hidden">AI 正在思考...</div>
</div> </div>
<script src="${scriptUri}"></script> <script src="${scriptUri}"></script>
<script src="${highlightScriptUri}"></script>
<script src="${markedScriptUri}"></script>
</body> </body>
</html> </html>

@ -1,4 +1,5 @@
let selectedCodeForContext = ''; let selectedCodeForContext = '';
let contextFilePath = ''; // 存储当前上下文文件路径
const vscode = acquireVsCodeApi(); const vscode = acquireVsCodeApi();
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@ -7,6 +8,12 @@ document.addEventListener('DOMContentLoaded', () => {
const chatBox = document.getElementById('chat-box'); const chatBox = document.getElementById('chat-box');
const loading = document.getElementById('loading'); const loading = document.getElementById('loading');
// 初始化隐藏工作区
const workspaceContainer = document.querySelector('.workspace-container');
if (workspaceContainer) {
workspaceContainer.style.display = 'none';
}
chatForm.addEventListener('submit', (e) => { chatForm.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
const text = userInput.value.trim(); const text = userInput.value.trim();
@ -15,7 +22,8 @@ document.addEventListener('DOMContentLoaded', () => {
vscode.postMessage({ vscode.postMessage({
command: 'ask', command: 'ask',
text, text,
fileContent: selectedCodeForContext fileContent: selectedCodeForContext,
fileContentPath: contextFilePath // 添加文件路径
}); });
// addMessage('user', text); // addMessage('user', text);
@ -70,7 +78,9 @@ document.addEventListener('DOMContentLoaded', () => {
// 高亮代码块 // 高亮代码块
const codeBlocks = msgDiv.querySelectorAll('pre code'); const codeBlocks = msgDiv.querySelectorAll('pre code');
codeBlocks.forEach(block => { codeBlocks.forEach(block => {
if (hljs) {
hljs.highlightElement(block); hljs.highlightElement(block);
}
}); });
// 滚动到底部 // 滚动到底部
@ -86,27 +96,82 @@ document.addEventListener('DOMContentLoaded', () => {
// 显示添加的行 // 显示添加的行
if (codeDiff.added && codeDiff.added.length > 0) { if (codeDiff.added && codeDiff.added.length > 0) {
codeDiff.added.forEach(line => { codeDiff.added.forEach((line, index) => {
const diffRow = document.createElement('div');
diffRow.className = 'diff-row';
// 修复行号显示问题
const lineNumber = document.createElement('div');
lineNumber.className = 'diff-line-number';
lineNumber.textContent = index + 1; // 使用索引+1作为行号
const diffLine = document.createElement('div'); const diffLine = document.createElement('div');
diffLine.className = 'diff-line diff-added'; diffLine.className = 'diff-line diff-added';
diffLine.textContent = `+ ${line}`; diffLine.textContent = `+ ${line}`;
diffContent.appendChild(diffLine);
diffRow.appendChild(lineNumber);
diffRow.appendChild(diffLine);
diffContent.appendChild(diffRow);
}); });
} }
// 显示删除的行 // 显示删除的行
if (codeDiff.removed && codeDiff.removed.length > 0) { if (codeDiff.removed && codeDiff.removed.length > 0) {
codeDiff.removed.forEach(line => { codeDiff.removed.forEach((line, index) => {
const diffRow = document.createElement('div');
diffRow.className = 'diff-row';
// 修复行号显示问题
const lineNumber = document.createElement('div');
lineNumber.className = 'diff-line-number';
lineNumber.textContent = index + 1; // 使用索引+1作为行号
const diffLine = document.createElement('div'); const diffLine = document.createElement('div');
diffLine.className = 'diff-line diff-removed'; diffLine.className = 'diff-line diff-removed';
diffLine.textContent = `- ${line}`; diffLine.textContent = `- ${line}`;
diffContent.appendChild(diffLine);
diffRow.appendChild(lineNumber);
diffRow.appendChild(diffLine);
diffContent.appendChild(diffRow);
}); });
} }
// 添加接受和拒绝按钮的事件监听
document.getElementById('accept-changes-btn').onclick = () => acceptChanges(codeDiff.modifiedCode);
document.getElementById('reject-changes-btn').onclick = () => rejectChanges();
diffModal.classList.remove('hidden'); diffModal.classList.remove('hidden');
} }
// 接受代码变更
function acceptChanges(modifiedCode) {
if (contextFilePath) {
// 发送消息到插件以接受变更
vscode.postMessage({
command: 'acceptChanges',
filePath: contextFilePath,
modifiedContent: modifiedCode
});
// 关闭模态框
document.getElementById('diff-modal').classList.add('hidden');
}
}
// 拒绝代码变更
function rejectChanges() {
if (contextFilePath) {
// 发送消息到插件以拒绝变更
vscode.postMessage({
command: 'rejectChanges',
filePath: contextFilePath
});
// 关闭模态框
document.getElementById('diff-modal').classList.add('hidden');
}
}
function showLoading() { function showLoading() {
loading.classList.remove('hidden'); loading.classList.remove('hidden');
} }
@ -115,6 +180,65 @@ document.addEventListener('DOMContentLoaded', () => {
loading.classList.add('hidden'); loading.classList.add('hidden');
} }
// 更新工作区文件列表
function updateWorkspaceFiles(files) {
const workspaceContainer = document.querySelector('.workspace-container');
const workspaceFilesContainer = document.getElementById('workspace-files');
// 如果没有文件变更,则隐藏工作区
if (!files || Object.keys(files).length === 0) {
workspaceContainer.style.display = 'none';
return;
}
// 显示工作区
workspaceContainer.style.display = 'block';
workspaceFilesContainer.innerHTML = '';
Object.keys(files).forEach(filePath => {
const fileChange = files[filePath];
const fileItem = document.createElement('div');
fileItem.className = 'workspace-file-item';
// 获取文件名
const fileName = filePath.split(/[\/\\]/).pop();
// 根据状态设置不同的显示样式
let statusText = '';
let statusClass = '';
switch (fileChange.status) {
case 'accepted':
statusText = '✓ 已接受';
statusClass = 'status-accepted';
break;
case 'rejected':
statusText = '✗ 已拒绝';
statusClass = 'status-rejected';
break;
default:
statusText = '待处理';
statusClass = 'status-pending';
}
fileItem.innerHTML = `
<span class="file-icon">📄</span>
<span class="file-name" title="${filePath}">${fileName}</span>
<span class="file-status ${statusClass}">${statusText}</span>
`;
fileItem.addEventListener('click', () => {
// 发送消息到插件以打开文件
vscode.postMessage({
command: 'openWorkspaceFile',
filePath: filePath
});
});
workspaceFilesContainer.appendChild(fileItem);
});
}
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
const message = event.data; const message = event.data;
console.log(message) console.log(message)
@ -157,6 +281,12 @@ document.addEventListener('DOMContentLoaded', () => {
// 关闭模态框 // 关闭模态框
document.getElementById('file-browser-modal').classList.add('hidden'); document.getElementById('file-browser-modal').classList.add('hidden');
// 清空文件搜索输入框
document.getElementById('file-search-input').value = '';
// 保存当前上下文文件路径
contextFilePath = file.path;
}); });
fileListContainer.appendChild(fileItem); fileListContainer.appendChild(fileItem);
@ -171,10 +301,27 @@ document.addEventListener('DOMContentLoaded', () => {
fileNameElement.innerText = fileName; fileNameElement.innerText = fileName;
fileNameElement.title = fullPath; fileNameElement.title = fullPath;
selectedCodeForContext = message.fileContent; selectedCodeForContext = message.fileContent;
contextFilePath = fullPath; // 保存上下文文件路径
// 隐藏占位符,显示文件标签 // 隐藏占位符,显示文件标签
contextPlaceholder.classList.add('hidden'); contextPlaceholder.classList.add('hidden');
contextTab.classList.remove('hidden'); contextTab.classList.remove('hidden');
} else if (message.command === 'updateWorkspaceFiles') {
// 更新工作区文件列表
updateWorkspaceFiles(message.files);
} else if (message.command === 'restoreState') {
// 恢复面板状态
currentSessionHistory = message.history || [];
// 恢复显示历史消息
currentSessionHistory.forEach(msg => {
addMessage(msg.role, msg.content, null);
});
// 恢复工作区变更
if (message.workspaceChanges) {
updateWorkspaceFiles(message.workspaceChanges);
}
} }
}); });
@ -221,6 +368,7 @@ document.addEventListener('DOMContentLoaded', () => {
contextTab.classList.add('hidden'); contextTab.classList.add('hidden');
contextPlaceholder.classList.remove('hidden'); contextPlaceholder.classList.remove('hidden');
selectedCodeForContext = ''; selectedCodeForContext = '';
contextFilePath = '';
}); });
// 关闭差异模态框 // 关闭差异模态框
document.getElementById('close-diff-modal').addEventListener('click', () => { document.getElementById('close-diff-modal').addEventListener('click', () => {

@ -1,214 +1,191 @@
/* 基础样式 */ /* 基础样式 */
body { body {
background-color: #282c34; font-family: var(--vscode-font-family);
color: white; font-size: var(--vscode-font-size);
background-color: var(--vscode-editor-background);
color: var(--vscode-editor-foreground);
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100vh;
display: flex;
flex-direction: column;
} }
/* 聊天容器 */
.chat-container { .chat-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
overflow: hidden; padding: 10px;
box-sizing: border-box;
} }
/* 对话区域 */
#chat-box { #chat-box {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
margin-bottom: 10px;
padding: 10px; padding: 10px;
border-bottom: 1px solid #444; border-radius: 4px;
background-color: var(--vscode-editor-background);
} }
.user, /* 消息样式 */
.ai { .message {
align-self: flex-end; margin-bottom: 15px;
color: #000000; padding: 8px 12px;
margin: 7px 3px; border-radius: 4px;
padding: 7px; line-height: 1.4;
border-radius: 10px;
font-size: 14px;
} }
.user { .message.user {
background-color: #717a76; background-color: var(--vscode-textBlockQuote-background);
align-self: flex-end;
margin-left: 20%;
} }
.ai { .message.ai {
background-color: #7a7575; background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-editorWidget-border);
} }
.context-area { /* 代码块样式 */
background-color: #3e4451; pre {
background-color: var(--vscode-textCodeBlock-background);
padding: 10px; padding: 10px;
border: 1px solid #61afef;
border-radius: 4px; border-radius: 4px;
margin: 10px; overflow-x: auto;
display: flex;
align-items: center;
justify-content: space-between;
}
#add-context-btn {
background-color: transparent;
border: none;
color: #61afef;
cursor: pointer;
font-size: 16px;
}
.context-options {
display: flex;
align-items: center;
} }
.context-options span, code {
.context-options select, font-family: var(--vscode-editor-font-family);
.context-options label { font-size: var(--vscode-editor-font-size);
margin-right: 10px;
}
.keybinds {
font-size: 12px;
color: #abb2bf;
} }
/* 表单区域 */
#chat-form { #chat-form {
display: flex; display: flex;
padding: 3px 10px; gap: 10px;
background-color: #282c34; margin-top: auto;
/* border-top: 1px solid #444; */ padding: 10px 0;
} }
#user-input { #user-input {
flex: 1; flex: 1;
padding: 10px; padding: 8px 12px;
border: none; border: 1px solid var(--vscode-dropdown-border);
background-color: #3e4451; border-radius: 4px;
color: white; background-color: var(--vscode-input-background);
font-size: 14px; color: var(--vscode-input-foreground);
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
}
#user-input:focus {
outline: none; outline: none;
border-color: var(--vscode-focusBorder);
} }
#chat-form button { #chat-form button {
padding: 10px; padding: 8px 16px;
background-color: #61afef; background-color: var(--vscode-button-background);
color: white; color: var(--vscode-button-foreground);
border: none; border: none;
border-radius: 4px;
cursor: pointer; cursor: pointer;
margin-left: 10px; font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
} }
#chat-form button:hover {
background-color: var(--vscode-button-hoverBackground);
}
/* 加载提示 */
#loading { #loading {
text-align: center; text-align: center;
padding: 10px; padding: 10px;
background-color: #3e4451; color: var(--vscode-descriptionForeground);
color: white;
border: 1px solid #61afef;
border-radius: 4px;
margin: 10px;
} }
.hidden { #loading.hidden {
display: none; display: none;
} }
/* 上下文容器 */
.context-container { .context-container {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 10px 0; margin-bottom: 10px;
gap: 10px; /* 元素间的间距 */ gap: 5px;
padding: 1px 10px;
} }
.context-tab { .select-context-btn {
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #007acc;
color: white;
border: none;
cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 6px 10px; justify-content: center;
background-color: #e9e9e9; font-size: 16px;
border-radius: 4px; padding: 0;
font-size: 13px;
min-width: 0; /* 允许收缩 */
flex: 0 1 auto; /* 不要占据多余空间 */
color: #000000;
cursor: default;
}
.context-tab.hidden {
display: none;
} }
.file-name { .select-context-btn:hover {
flex: 1; background-color: #005a9e;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 10px;
} }
.context-placeholder { .context-placeholder {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 6px 2px; padding: 4px 8px;
color: #666; border-radius: 4px;
font-size: 13px; background-color: var(--vscode-badge-background);
min-width: 0; color: var(--vscode-badge-foreground);
font-size: 12px;
} }
.placeholder-text { .context-tab {
overflow: hidden; display: flex;
text-overflow: ellipsis; align-items: center;
white-space: nowrap; padding: 4px 8px;
border-radius: 4px;
background-color: var(--vscode-tab-activeBackground);
color: var(--vscode-tab-activeForeground);
font-size: 12px;
} }
.context-placeholder.hidden { .context-tab.hidden {
display: none; display: none;
} }
.file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
.close-btn { .close-btn {
background: none; background: none;
border: none; border: none;
font-size: 18px; color: var(--vscode-icon-foreground);
cursor: pointer; cursor: pointer;
color: #666;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.close-btn:hover {
color: #ff0000;
background-color: rgba(0,0,0,0.1);
border-radius: 50%;
}
.select-context-btn {
width: 24px;
height: 24px;
padding: 0;
background-color: #007acc;
color: white;
border: none;
border-radius: 50%;
font-size: 16px; font-size: 16px;
font-weight: bold; width: 16px;
cursor: pointer; height: 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; padding: 0;
}
.select-context-btn:hover {
background-color: #005a9e;
} }
/* 模态框样式 */ /* 文件浏览器模态框 */
.modal { .modal {
position: fixed; position: fixed;
top: 0; top: 0;
@ -217,8 +194,8 @@ body {
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
justify-content: center;
z-index: 1000; z-index: 1000;
} }
@ -227,10 +204,11 @@ body {
} }
.modal-content { .modal-content {
background-color: #282c34; background-color: var(--vscode-editorWidget-background);
border-radius: 8px; border: 1px solid var(--vscode-editorWidget-border);
width: 80%; border-radius: 4px;
max-width: 600px; width: 90%;
max-width: 500px;
max-height: 80vh; max-height: 80vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -240,88 +218,73 @@ body {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 15px 20px; padding: 10px 15px;
border-bottom: 1px solid #444; border-bottom: 1px solid var(--vscode-editorWidget-border);
} }
.modal-header h3 { .modal-header h3 {
margin: 0; margin: 0;
color: white; font-size: 16px;
font-weight: 600;
} }
.close-modal { .close-modal {
font-size: 24px;
cursor: pointer; cursor: pointer;
color: #aaa; font-size: 20px;
} color: var(--vscode-icon-foreground);
.close-modal:hover {
color: white;
} }
.modal-body { .modal-body {
flex: 1; flex: 1;
overflow-y: auto;
padding: 15px; padding: 15px;
overflow: hidden;
display: flex;
flex-direction: column;
} }
.file-search { .file-search {
flex: 1;
margin-bottom: 15px; margin-bottom: 15px;
} }
.file-search input { .file-search input {
padding: 8px; width: 100%;
background-color: #3e4451; padding: 6px 10px;
border: 1px solid #444; border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px; border-radius: 4px;
color: white; background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
box-sizing: border-box;
} }
.file-list { .file-list {
flex: 1; display: flex;
overflow-y: auto; flex-direction: column;
border: 1px solid #444; gap: 5px;
border-radius: 4px;
background-color: #3e4451;
} }
.file-item { .file-item {
padding: 10px;
border-bottom: 1px solid #444;
cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
} }
.file-item:hover { .file-item:hover {
background-color: #4e5461; background-color: var(--vscode-list-hoverBackground);
}
.file-item:last-child {
border-bottom: none;
} }
.file-icon { .file-icon {
margin-right: 10px; margin-right: 8px;
width: 16px;
text-align: center;
}
.file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.loading { .loading {
text-align: center; text-align: center;
padding: 20px; padding: 20px;
color: #abb2bf; color: var(--vscode-descriptionForeground);
} }
/* 差异模态框 */
.diff-modal-content { .diff-modal-content {
width: 90%; width: 90%;
max-width: 900px; max-width: 900px;
@ -331,13 +294,28 @@ body {
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-size: 14px; font-size: 14px;
line-height: 1.4; line-height: 1.4;
background-color: #1e1e1e; background-color: var(--vscode-editor-background);
padding: 15px; padding: 15px;
border-radius: 4px; border-radius: 4px;
overflow-x: auto; overflow-x: auto;
margin-bottom: 15px;
max-height: 60vh;
}
.diff-row {
display: flex;
}
.diff-line-number {
width: 40px;
text-align: right;
padding-right: 10px;
color: var(--vscode-descriptionForeground);
user-select: none;
} }
.diff-line { .diff-line {
flex: 1;
white-space: pre; white-space: pre;
} }
@ -352,8 +330,35 @@ body {
} }
.diff-unchanged { .diff-unchanged {
color: #d4d4d4; color: var(--vscode-editor-foreground);
}
.diff-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 10px 0;
}
.diff-action-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: var(--vscode-font-family);
font-size: 12px;
} }
.diff-action-btn.accept {
background-color: #007acc;
color: white;
}
.diff-action-btn.reject {
background-color: #d73a49;
color: white;
}
.diff-button { .diff-button {
margin-top: 10px; margin-top: 10px;
padding: 5px 10px; padding: 5px 10px;
@ -368,3 +373,75 @@ body {
.diff-button:hover { .diff-button:hover {
background-color: #005a9e; background-color: #005a9e;
} }
/* 工作区容器 */
.workspace-container {
margin: 10px 0;
padding: 10px;
border: 1px solid var(--vscode-editorWidget-border);
border-radius: 4px;
background-color: var(--vscode-editor-background);
}
.workspace-container h3 {
margin: 0 0 10px 0;
font-size: 14px;
font-weight: 600;
}
.workspace-files {
max-height: 150px;
overflow-y: auto;
}
.workspace-file-item {
display: flex;
align-items: center;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
}
.workspace-file-item:hover {
background-color: var(--vscode-list-hoverBackground);
}
.workspace-file-item .file-icon {
margin-right: 8px;
}
.workspace-file-item .file-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.workspace-file-item .file-status {
font-size: 11px;
padding: 2px 6px;
border-radius: 3px;
margin-left: 8px;
}
.file-status.status-pending {
background-color: #007acc;
color: white;
}
.file-status.status-accepted {
background-color: #28a745;
color: white;
}
.file-status.status-rejected {
background-color: #d73a49;
color: white;
}
.no-changes {
text-align: center;
padding: 10px;
color: var(--vscode-descriptionForeground);
font-size: 12px;
}

@ -2,9 +2,17 @@ import * as vscode from 'vscode';
import * as path from 'path'; import * as path from 'path';
import * as diff from 'diff'; import * as diff from 'diff';
// 定义全局变量来存储面板状态
let panel: vscode.WebviewPanel | undefined; let panel: vscode.WebviewPanel | undefined;
let currentSessionHistory: { role: string; content: string }[] = []; let currentSessionHistory: { role: string; content: string }[] = [];
// 存储工作区变更的文件,包含状态信息
let workspaceChanges: {
[key: string]: {
original: string;
modified: string;
status: 'pending' | 'accepted' | 'rejected'
}
} = {};
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
console.log('Extension activated'); console.log('Extension activated');
@ -130,8 +138,19 @@ function openWebview(
globalState: vscode.Memento, globalState: vscode.Memento,
context: vscode.ExtensionContext // 添加 context 参数 context: vscode.ExtensionContext // 添加 context 参数
) { ) {
// 每次打开新的 Webview 时清空当前会话历史 // 如果面板已经存在,直接显示它而不是创建新的
currentSessionHistory = []; 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( panel = vscode.window.createWebviewPanel(
'aiChatWebview', 'aiChatWebview',
@ -187,6 +206,23 @@ function openWebview(
const codeDiff = generateCodeDiff(fileContent, aiMessage); const codeDiff = generateCodeDiff(fileContent, aiMessage);
console.log("codeDiff:",codeDiff) console.log("codeDiff:",codeDiff)
// 如果有代码差异,保存到工作区变更中
if (codeDiff && fileContent && message.fileContentPath) {
workspaceChanges[message.fileContentPath] = {
original: fileContent,
modified: codeDiff.modifiedCode,
status: 'pending'
};
// 通知 WebView 更新工作区文件列表
if (panel && panel.webview) {
panel.webview.postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
}
}
// 更新当前会话历史 // 更新当前会话历史
currentSessionHistory = [...currentSessionHistory, { role: 'assistant', content: aiMessage }]; currentSessionHistory = [...currentSessionHistory, { role: 'assistant', content: aiMessage }];
@ -262,16 +298,139 @@ function openWebview(
vscode.window.showErrorMessage(`无法读取文件: ${error.message}`); vscode.window.showErrorMessage(`无法读取文件: ${error.message}`);
} }
break; break;
case 'showCodeDiffInEditor':
// 在编辑器中显示代码差异
try {
const originalCode = message.originalCode || '';
const modifiedCode = message.modifiedCode || '';
// 创建原始代码的虚拟文档
const originalUri = vscode.Uri.parse('untitled:original-code.txt');
const originalDoc = await vscode.workspace.openTextDocument(originalUri);
const originalEditor = await vscode.window.showTextDocument(originalDoc, vscode.ViewColumn.One, true);
await originalEditor.edit(edit => {
edit.insert(new vscode.Position(0, 0), originalCode);
});
// 创建修改后代码的虚拟文档
const modifiedUri = vscode.Uri.parse('untitled:modified-code.txt');
const modifiedDoc = await vscode.workspace.openTextDocument(modifiedUri);
const modifiedEditor = await vscode.window.showTextDocument(modifiedDoc, vscode.ViewColumn.Two, true);
await modifiedEditor.edit(edit => {
edit.insert(new vscode.Position(0, 0), modifiedCode);
});
// 使用 VS Code 的 diff 命令显示差异
await vscode.commands.executeCommand(
'vscode.diff',
originalUri,
modifiedUri,
'Code Diff - Original ↔ Modified'
);
} catch (error) {
vscode.window.showErrorMessage(`显示代码差异失败: ${error.message}`);
}
break;
case 'acceptChanges':
// 接受代码变更
try {
const { filePath, modifiedContent } = message;
const fileUri = vscode.Uri.file(filePath);
// 将修改后的内容写入文件
const encodedContent = new TextEncoder().encode(modifiedContent);
await vscode.workspace.fs.writeFile(fileUri, encodedContent);
// 更新工作区变更状态
if (workspaceChanges[filePath]) {
workspaceChanges[filePath].status = 'accepted';
}
// 通知 WebView 更新工作区文件列表
if (panel && panel.webview) {
panel.webview.postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
}
vscode.window.showInformationMessage('代码变更已应用');
} catch (error) {
vscode.window.showErrorMessage(`应用代码变更失败: ${error.message}`);
}
break;
case 'rejectChanges':
// 拒绝代码变更
try {
const { filePath } = message;
// 更新工作区变更状态
if (workspaceChanges[filePath]) {
workspaceChanges[filePath].status = 'rejected';
}
// 通知 WebView 更新工作区文件列表
if (panel && panel.webview) {
panel.webview.postMessage({
command: 'updateWorkspaceFiles',
files: workspaceChanges
});
}
vscode.window.showInformationMessage('代码变更已拒绝');
} catch (error) {
vscode.window.showErrorMessage(`拒绝代码变更失败: ${error.message}`);
}
break;
case 'openWorkspaceFile':
// 打开工作区文件
try {
const { filePath } = message;
const fileUri = vscode.Uri.file(filePath);
// 检查文件是否已经打开
let fileAlreadyOpen = false;
const openedTextDocuments = vscode.workspace.textDocuments;
for (const doc of openedTextDocuments) {
if (doc.uri.fsPath === fileUri.fsPath) {
fileAlreadyOpen = true;
// 激活已打开的文件
await vscode.window.showTextDocument(doc, {
viewColumn: vscode.ViewColumn.One // 切换到已打开的文件
});
break;
}
}
// 如果文件未打开,则在新窗口打开
if (!fileAlreadyOpen) {
await vscode.window.showTextDocument(fileUri, {
viewColumn: vscode.ViewColumn.One // 在新窗口打开文件
});
}
} catch (error) {
vscode.window.showErrorMessage(`打开文件失败: ${error.message}`);
}
break;
case 'getCodeChanges':
// 获取代码变更
if (panel && panel.webview) {
panel.webview.postMessage({
command: 'codeChanges',
changes: workspaceChanges
});
}
break;
} }
}, },
undefined, undefined,
context.subscriptions context.subscriptions
); );
// 添加 dispose 监听器 // // 添加 dispose 监听器
panel.onDidDispose(() => { panel.onDidDispose(() => {
// 面板关闭时清空当前会话历史 // 不再清空会话历史,让用户在重新打开面板时保留上下文
currentSessionHistory = [];
panel = undefined; panel = undefined;
}, null, context.subscriptions); }, null, context.subscriptions);
} }
@ -327,7 +486,7 @@ async function callQwenAPI(
} }
} }
function generateCodeDiff(originalCode: string, aiResponse: string): { added: string[]; removed: string[] } | null { function generateCodeDiff(originalCode: string, aiResponse: string): { added: string[]; removed: string[]; modifiedCode: string } | null {
console.log("模型返回值:",aiResponse) console.log("模型返回值:",aiResponse)
// 提取代码块中的代码 // 提取代码块中的代码
const codeBlockRegex = /```(?:([a-zA-Z0-9]+))?\s*[\n\r]([\s\S]*?)\s*```/g; const codeBlockRegex = /```(?:([a-zA-Z0-9]+))?\s*[\n\r]([\s\S]*?)\s*```/g;
@ -370,10 +529,12 @@ function generateCodeDiff(originalCode: string, aiResponse: string): { added: st
} }
}); });
// 只有当确实有差异时才返回 // 返回差异结果,无论是否有差异
if (added.length > 0 || removed.length > 0) { return {
return { added, removed }; added,
} removed,
modifiedCode: normalizedModified
};
} }
return null; return null;
@ -409,6 +570,14 @@ function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlight
</div> </div>
</div> </div>
<!-- -->
<div class="workspace-container" style="display: none;">
<h3></h3>
<div id="workspace-files" class="workspace-files">
<div class="no-changes"></div>
</div>
</div>
<!-- --> <!-- -->
<div id="file-browser-modal" class="modal hidden"> <div id="file-browser-modal" class="modal hidden">
<div class="modal-content"> <div class="modal-content">
@ -436,6 +605,10 @@ function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlight
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div id="diff-content" class="diff-content"></div> <div id="diff-content" class="diff-content"></div>
<div class="diff-actions">
<button id="accept-changes-btn" class="diff-action-btn accept"></button>
<button id="reject-changes-btn" class="diff-action-btn reject"></button>
</div>
</div> </div>
</div> </div>
</div> </div>

Loading…
Cancel
Save