diff --git a/src/services/sseHandler.ts b/src/services/sseHandler.ts index b6aba4b..d150c1f 100644 --- a/src/services/sseHandler.ts +++ b/src/services/sseHandler.ts @@ -13,6 +13,8 @@ import type { SSEEventType, TextDeltaEvent, ToolCallRequest, + ToolConfirmEvent, + PlanConfirmEvent, AskUserEvent, CompleteEvent, ErrorEvent, @@ -36,6 +38,10 @@ export interface SSECallbacks { onTextDelta?: (data: TextDeltaEvent) => void; /** 收到工具调用请求 */ onToolCall?: (data: ToolCallRequest) => void; + /** 收到工具确认请求(Ask 模式) */ + onToolConfirm?: (data: ToolConfirmEvent) => void; + /** 收到计划确认请求(Plan 模式) */ + onPlanConfirm?: (data: PlanConfirmEvent) => void; /** 工具开始执行 */ onToolStart?: (data: ToolStartEvent) => void; /** 工具执行完成 */ @@ -136,7 +142,7 @@ export async function startStreamDialog( const body = JSON.stringify(request); - console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, url=${urlString}`); + console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, mode=${request.mode}, url=${urlString}`); return new Promise((resolve, reject) => { const options: http.RequestOptions = { @@ -268,6 +274,12 @@ function dispatchEvent( case 'tool_call': callbacks.onToolCall?.(data as ToolCallRequest); break; + case 'tool_confirm': + callbacks.onToolConfirm?.(data as ToolConfirmEvent); + break; + case 'plan_confirm': + callbacks.onPlanConfirm?.(data as PlanConfirmEvent); + break; case 'tool_start': callbacks.onToolStart?.(data as ToolStartEvent); break; diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index 89f2d39..635f8d2 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -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) => + \`
\${i + 1}. \${step}
\` + ).join(''); + + // 选项按钮 + const options = ['确认执行', '修改计划', '取消']; + const optionsHtml = options.map(opt => { + const isSelected = isAnswered && opt === selectedAnswer; + return \`\`; + }).join(''); + + segmentDiv.innerHTML = \` +
+
+ 📋 + \${segment.planTitle || '执行计划'} +
+
+
\${segment.planSummary || ''}
+
\${stepsHtml}
+
+
+
\${optionsHtml}
+
+ + +
+
+
+ \`; + + // 只在未回答时添加事件监听 + 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 { \`; + } else if (segment.type === 'plan') { + // 计划卡片渲染 + segmentDiv.className += ' segment-plan'; + const stepsHtml = (segment.planSteps || []).map((step, i) => + \`
\${i + 1}. \${step}
\` + ).join(''); + + segmentDiv.innerHTML = \` +
+
+ 📋 + \${segment.planTitle || '执行计划'} +
+
+
\${segment.planSummary || ''}
+
\${stepsHtml}
+
+
+ + + +
+
+ \`; + + // 绑定按钮事件 + 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);