feat: SSE 事件处理和计划确认 UI
- sseHandler 添加 onPlanConfirm、onToolConfirm 回调 - messageArea 添加计划确认对话框渲染
This commit is contained in:
@ -13,6 +13,8 @@ import type {
|
|||||||
SSEEventType,
|
SSEEventType,
|
||||||
TextDeltaEvent,
|
TextDeltaEvent,
|
||||||
ToolCallRequest,
|
ToolCallRequest,
|
||||||
|
ToolConfirmEvent,
|
||||||
|
PlanConfirmEvent,
|
||||||
AskUserEvent,
|
AskUserEvent,
|
||||||
CompleteEvent,
|
CompleteEvent,
|
||||||
ErrorEvent,
|
ErrorEvent,
|
||||||
@ -36,6 +38,10 @@ export interface SSECallbacks {
|
|||||||
onTextDelta?: (data: TextDeltaEvent) => void;
|
onTextDelta?: (data: TextDeltaEvent) => void;
|
||||||
/** 收到工具调用请求 */
|
/** 收到工具调用请求 */
|
||||||
onToolCall?: (data: ToolCallRequest) => void;
|
onToolCall?: (data: ToolCallRequest) => void;
|
||||||
|
/** 收到工具确认请求(Ask 模式) */
|
||||||
|
onToolConfirm?: (data: ToolConfirmEvent) => void;
|
||||||
|
/** 收到计划确认请求(Plan 模式) */
|
||||||
|
onPlanConfirm?: (data: PlanConfirmEvent) => void;
|
||||||
/** 工具开始执行 */
|
/** 工具开始执行 */
|
||||||
onToolStart?: (data: ToolStartEvent) => void;
|
onToolStart?: (data: ToolStartEvent) => void;
|
||||||
/** 工具执行完成 */
|
/** 工具执行完成 */
|
||||||
@ -136,7 +142,7 @@ export async function startStreamDialog(
|
|||||||
|
|
||||||
const body = JSON.stringify(request);
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const options: http.RequestOptions = {
|
const options: http.RequestOptions = {
|
||||||
@ -268,6 +274,12 @@ function dispatchEvent(
|
|||||||
case 'tool_call':
|
case 'tool_call':
|
||||||
callbacks.onToolCall?.(data as ToolCallRequest);
|
callbacks.onToolCall?.(data as ToolCallRequest);
|
||||||
break;
|
break;
|
||||||
|
case 'tool_confirm':
|
||||||
|
callbacks.onToolConfirm?.(data as ToolConfirmEvent);
|
||||||
|
break;
|
||||||
|
case 'plan_confirm':
|
||||||
|
callbacks.onPlanConfirm?.(data as PlanConfirmEvent);
|
||||||
|
break;
|
||||||
case 'tool_start':
|
case 'tool_start':
|
||||||
callbacks.onToolStart?.(data as ToolStartEvent);
|
callbacks.onToolStart?.(data as ToolStartEvent);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -611,6 +611,88 @@ export function getMessageAreaStyles(): string {
|
|||||||
text-align: center;
|
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()}
|
${getWaveformPreviewContent()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -1034,6 +1116,93 @@ export function getMessageAreaScript(): string {
|
|||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 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);
|
currentSegmentedMessage.appendChild(segmentDiv);
|
||||||
@ -1203,6 +1372,47 @@ export function getMessageAreaScript(): string {
|
|||||||
</div>
|
</div>
|
||||||
</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);
|
container.appendChild(segmentDiv);
|
||||||
|
|||||||
Reference in New Issue
Block a user