Merge branch 'feat/back-to-front' into feat/plugin-front-end

This commit is contained in:
Roe-xin
2025-12-30 20:46:26 +08:00
12 changed files with 756 additions and 54 deletions

View File

@ -47,7 +47,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
(message) => {
switch (message.command) {
case "sendMessage":
handleUserMessage(panel, message.text, context.extensionPath);
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
break;
case "readFile":
handleReadFile(panel, message.filePath);

View File

@ -1,6 +1,12 @@
/**
* 模式选择器组件
* 提供 Agent/Ask/Auto 种模式的选择功能
* 提供 Plan/Ask/Agent/Auto 种模式的选择功能
*
* 模式说明:
* - Plan: 只读模式,只能查询分析,不能写文件
* - Ask: 逐个确认,每个写操作需用户确认
* - Agent: 智能体自主,自动执行大部分操作
* - Auto: 完全自动,所有操作自动执行
*/
/**
@ -17,12 +23,25 @@ export function getModeSelectorContent(): string {
</svg>
</div>
<div class="mode-dropdown" id="modeDropdown">
<div class="mode-option" data-value="agent" onclick="selectMode('agent', 'Agent')">Agent</div>
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">Ask</div>
<div class="mode-option" data-value="auto" onclick="selectMode('auto', 'Auto')">Auto</div>
<div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')">
<span class="mode-option-label">Plan</span>
<span class="mode-option-desc">只读模式</span>
</div>
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">
<span class="mode-option-label">Ask</span>
<span class="mode-option-desc">逐个确认</span>
</div>
<div class="mode-option selected" data-value="agent" onclick="selectMode('agent', 'Agent')">
<span class="mode-option-label">Agent</span>
<span class="mode-option-desc">智能体自主</span>
</div>
<div class="mode-option" data-value="auto" onclick="selectMode('auto', 'Auto')">
<span class="mode-option-label">Auto</span>
<span class="mode-option-desc">完全自动</span>
</div>
</div>
</div>
<span class="tooltiptext">切换模式</span>
<span class="tooltiptext" id="modeTooltip">智能体自主模式</span>
</div>
`;
}
@ -69,7 +88,7 @@ export function getModeSelectorStyles(): string {
position: absolute;
bottom: calc(100% + 2px);
left: 0;
min-width: 100%;
min-width: 140px;
background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px;
@ -83,7 +102,10 @@ export function getModeSelectorStyles(): string {
}
/* 模式选择器的选项样式 */
.mode-option {
padding: 6px 12px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
font-size: 12px;
cursor: pointer;
transition: background 0.2s ease;
@ -93,8 +115,15 @@ export function getModeSelectorStyles(): string {
background: rgba(128, 128, 128, 0.3);
}
.mode-option.selected {
background: rgba(128, 128, 128, 0.5);
color: var(--vscode-foreground);
background: rgba(64, 158, 255, 0.2);
}
.mode-option-label {
font-weight: 500;
}
.mode-option-desc {
font-size: 10px;
color: var(--vscode-descriptionForeground);
margin-left: 12px;
}
`;
}
@ -124,10 +153,23 @@ export function getModeSelectorScript(): string {
function selectMode(value, label) {
currentMode = value;
const modeValue = document.getElementById('modeValue');
const modeTooltip = document.getElementById('modeTooltip');
if (modeValue) {
modeValue.textContent = label;
}
// 更新 tooltip
if (modeTooltip) {
const tooltipMap = {
'plan': '只读模式 - 只能查询分析',
'ask': '逐个确认 - 每个写操作需确认',
'agent': '智能体自主模式',
'auto': '完全自动 - 所有操作自动执行'
};
modeTooltip.textContent = tooltipMap[value] || '切换模式';
}
// 更新选中状态
const options = document.querySelectorAll('.mode-option');
options.forEach(option => {

View File

@ -261,7 +261,7 @@ export function getInputAreaStyles(): string {
*/
export function getInputAreaScript(): string {
return `
${getModeSelectorScript()}
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
${getModelSelectorScript()}
${getContextButtonScript()}
${getContextCompressScript()}
@ -345,13 +345,14 @@ export function getInputAreaScript(): string {
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
const planMode = document.getElementById('planToggle')?.checked || false;
addMessage(text, 'user');
// 切换按钮为暂停状态
setSendButtonState(true);
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model });
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model, planMode: planMode });
messageInput.value = '';
autoResizeTextarea(); // 重置输入框高度
messageInput.focus();

View File

@ -611,6 +611,88 @@ export function getMessageAreaStyles(): string {
text-align: center;
}
/* 计划卡片样式 */
.segment-plan {
margin: 8px 0;
}
.plan-card {
border: 1px solid var(--vscode-input-border);
border-radius: 8px;
overflow: hidden;
background: var(--vscode-editor-background);
}
.plan-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: var(--vscode-sideBar-background);
border-bottom: 1px solid var(--vscode-input-border);
}
.plan-icon {
font-size: 18px;
}
.plan-title {
font-weight: 600;
font-size: 14px;
}
.plan-body {
padding: 12px;
}
.plan-summary {
color: var(--vscode-descriptionForeground);
margin-bottom: 10px;
font-size: 13px;
}
.plan-steps {
font-size: 13px;
}
.plan-step {
padding: 6px 8px;
margin-bottom: 4px;
background: var(--vscode-list-hoverBackground);
border-radius: 4px;
}
.plan-step:last-child {
margin-bottom: 0;
}
.step-num {
color: var(--vscode-textLink-foreground);
font-weight: 500;
margin-right: 4px;
}
.plan-actions {
display: flex;
gap: 8px;
padding: 12px;
border-top: 1px solid var(--vscode-input-border);
background: var(--vscode-sideBar-background);
}
.plan-btn {
padding: 6px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 12px;
font-weight: 500;
}
.plan-btn-confirm {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
.plan-btn-confirm:hover {
background: var(--vscode-button-hoverBackground);
}
.plan-btn-modify {
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border: 1px solid var(--vscode-input-border);
}
.plan-btn-cancel {
background: transparent;
color: var(--vscode-descriptionForeground);
}
${getWaveformPreviewContent()}
`;
}
@ -1034,6 +1116,93 @@ export function getMessageAreaScript(): string {
container.scrollTop = container.scrollHeight;
}
}, 0);
} else if (segment.type === 'plan') {
// 计划卡片渲染(类似 askUser
segmentDiv.className += ' segment-plan';
// 检查是否已回答
const isAnswered = answeredQuestions.has(segment.askId);
const selectedAnswer = answeredQuestions.get(segment.askId);
if (isAnswered) {
segmentDiv.classList.add('answered');
}
const stepsHtml = (segment.planSteps || []).map((step, i) =>
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
).join('');
// 选项按钮
const options = ['确认执行', '修改计划', '取消'];
const optionsHtml = options.map(opt => {
const isSelected = isAnswered && opt === selectedAnswer;
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
}).join('');
segmentDiv.innerHTML = \`
<div class="plan-card">
<div class="plan-header">
<span class="plan-icon">📋</span>
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
</div>
<div class="plan-body">
<div class="plan-summary">\${segment.planSummary || ''}</div>
<div class="plan-steps">\${stepsHtml}</div>
</div>
<div class="plan-actions">
<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
<input type="text" class="custom-input" placeholder="输入修改建议..." />
<button class="custom-submit">提交</button>
</div>
</div>
</div>
\`;
// 只在未回答时添加事件监听
if (!isAnswered) {
setTimeout(() => {
const optionButtons = segmentDiv.querySelectorAll('.question-option');
optionButtons.forEach(btn => {
btn.addEventListener('click', function() {
const option = this.getAttribute('data-option');
// 发送答案到后端
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
// 同时发送 planAction 用于模式切换
const actionMap = {
'确认执行': 'confirm',
'修改计划': 'modify',
'取消': 'cancel'
};
vscode.postMessage({
command: 'planAction',
action: actionMap[option] || option,
planTitle: segment.planTitle
});
});
});
const submitBtn = segmentDiv.querySelector('.custom-submit');
const customInput = segmentDiv.querySelector('.custom-input');
if (submitBtn && customInput) {
submitBtn.addEventListener('click', function() {
const customValue = customInput.value.trim();
if (customValue) {
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
}
});
customInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const customValue = customInput.value.trim();
if (customValue) {
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
}
}
});
}
}, 0);
}
}
currentSegmentedMessage.appendChild(segmentDiv);
@ -1203,6 +1372,47 @@ export function getMessageAreaScript(): string {
</div>
</div>
\`;
} else if (segment.type === 'plan') {
// 计划卡片渲染
segmentDiv.className += ' segment-plan';
const stepsHtml = (segment.planSteps || []).map((step, i) =>
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
).join('');
segmentDiv.innerHTML = \`
<div class="plan-card">
<div class="plan-header">
<span class="plan-icon">📋</span>
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
</div>
<div class="plan-body">
<div class="plan-summary">\${segment.planSummary || ''}</div>
<div class="plan-steps">\${stepsHtml}</div>
</div>
<div class="plan-actions">
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button>
<button class="plan-btn plan-btn-modify" data-action="modify">修改计划</button>
<button class="plan-btn plan-btn-cancel" data-action="cancel">取消</button>
</div>
</div>
\`;
// 绑定按钮事件
setTimeout(() => {
const planCard = segmentDiv.querySelector('.plan-card');
if (planCard) {
planCard.querySelectorAll('.plan-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const action = e.currentTarget?.dataset?.action;
vscode.postMessage({
command: 'planAction',
action: action,
planTitle: segment.planTitle
});
});
});
}
}, 0);
}
container.appendChild(segmentDiv);

View File

@ -1,19 +1,21 @@
/**
* Plan 开关组件
* 注意:功能已移至模式选择器,此组件仅保留样式(已禁用)
*/
/**
* 获取 Plan 开关的 HTML 内容
* 已禁用,仅保留样式展示
*/
export function getPlanToggleContent(): string {
return `
<div class="tooltip">
<label class="plan-toggle">
<input type="checkbox" id="planToggle" onchange="handlePlanToggle()">
<label class="plan-toggle plan-toggle-disabled">
<input type="checkbox" id="planToggle" disabled>
<span class="plan-toggle-slider"></span>
<span class="plan-toggle-label">Plan</span>
</label>
<span class="tooltiptext" id="planTooltip">启用 Plan 模式</span>
<span class="tooltiptext" id="planTooltip">请使用模式选择器切换 Plan 模式</span>
</div>
`;
}
@ -73,6 +75,17 @@ export function getPlanToggleStyles(): string {
font-weight: 500;
color: var(--vscode-foreground);
}
/* 禁用状态样式 */
.plan-toggle-disabled {
cursor: not-allowed;
opacity: 0.5;
}
.plan-toggle-disabled .plan-toggle-slider {
background: var(--vscode-input-background);
border-color: var(--vscode-input-border);
}
`;
}

View File

@ -255,7 +255,7 @@ export function getWebviewContent(iconUri?: string): string {
padding: 0;
}
.message-segment {
padding: 10px 22 px;
padding: 10px 22px;
}
.segment-text {
line-height: 1.6;
@ -417,6 +417,62 @@ export function getWebviewContent(iconUri?: string): string {
let loadingIndicator = null;
let currentSegmentedMessage = null; // 当前分段消息容器
// ========== 模式选择器脚本(直接内联,避免模板字符串嵌套问题)==========
let currentMode = 'agent';
function toggleModeDropdown() {
const modeSelectEl = document.getElementById('modeSelect');
const modelSelectEl = document.getElementById('modelSelect');
if (modeSelectEl) {
modeSelectEl.classList.toggle('active');
if (modelSelectEl) {
modelSelectEl.classList.remove('active');
}
}
}
function selectMode(value, label) {
currentMode = value;
const modeValue = document.getElementById('modeValue');
const modeTooltip = document.getElementById('modeTooltip');
if (modeValue) {
modeValue.textContent = label;
}
if (modeTooltip) {
const tooltipMap = {
'plan': '只读模式 - 只能查询分析',
'ask': '逐个确认 - 每个写操作需确认',
'agent': '智能体自主模式',
'auto': '完全自动 - 所有操作自动执行'
};
modeTooltip.textContent = tooltipMap[value] || '切换模式';
}
const options = document.querySelectorAll('.mode-option');
options.forEach(option => {
if (option.getAttribute('data-value') === value) {
option.classList.add('selected');
} else {
option.classList.remove('selected');
}
});
const modeSelectEl = document.getElementById('modeSelect');
if (modeSelectEl) {
modeSelectEl.classList.remove('active');
}
}
function getCurrentMode() {
return currentMode;
}
document.addEventListener('click', (event) => {
const modeSelectEl = document.getElementById('modeSelect');
if (modeSelectEl && !modeSelectEl.contains(event.target)) {
modeSelectEl.classList.remove('active');
}
});
// ========== 模式选择器脚本结束 ==========
function quickAction(type) {
const questions = {
counter: '生成一个4位同步计数器',
@ -594,6 +650,27 @@ export function getWebviewContent(iconUri?: string): string {
}
break;
case 'switchMode':
// 切换运行模式Plan 确认后自动切换到 Agent
if (message.mode && typeof selectMode === 'function') {
const labelMap = {
'plan': 'Plan',
'ask': 'Ask',
'agent': 'Agent',
'auto': 'Auto'
};
selectMode(message.mode, labelMap[message.mode] || message.mode);
console.log('[WebView] 模式已切换到:', message.mode);
}
break;
case 'addMessage':
// 添加消息(通用)
if (message.text && message.sender) {
addMessage(message.text, message.sender);
}
break;
default:
console.log('[WebView] 未处理的消息类型:', message.command);
}