diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 262aa2f..192ecd6 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -18,7 +18,7 @@ import { getUserIdFromToken } from '../utils/jwtUtils'; * 消息段落类型 */ export interface MessageSegment { - type: 'text' | 'tool' | 'question' | 'agent' | 'plan'; + type: 'text' | 'tool' | 'question' | 'agent' | 'plan' | 'progress'; content?: string; toolName?: string; toolStatus?: 'running' | 'success' | 'error'; @@ -33,8 +33,11 @@ export interface MessageSegment { agentSteps?: AgentStep[]; // 计划相关字段 planTitle?: string; + planPhases?: import('../types/api').PlanPhase[]; planSteps?: string[]; planSummary?: string; + // 进度条相关字段(独立于 plan,用于执行模式) + progressPhases?: import('../types/api').PlanPhase[]; } /** @@ -63,7 +66,7 @@ export interface DialogCallbacks { /** 工具确认请求(Ask 模式) */ onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record) => void; /** 计划确认请求(Plan 模式) */ - onPlanConfirm?: (confirmId: number, title: string, steps: string[], summary: string) => void; + onPlanConfirm?: (confirmId: number, title: string, phases: import('../types/api').PlanPhase[] | undefined, steps: string[] | undefined, summary: string) => void; /** 显示问题(ask_user) */ onQuestion?: (askId: string, question: string, options: string[]) => void; /** 实时更新段落(流式过程中) */ @@ -76,6 +79,8 @@ export interface DialogCallbacks { onNotification?: (message: string) => void; /** 上下文使用量更新 */ onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void; + /** 阶段进度更新 */ + onPhaseProgress?: (phaseId: string, status: string) => void; } /** @@ -530,10 +535,12 @@ export class DialogSession { const askId = `ask_${data.confirmId}`; // 添加计划段落到聊天界面(包含 askId 用于响应) + // 支持新格式(phases)和旧格式(steps) this.segments.push({ type: 'plan', askId: askId, planTitle: data.title, + planPhases: data.phases, planSteps: data.steps, planSummary: data.summary }); @@ -554,7 +561,108 @@ export class DialogSession { } // 调用回调通知 UI - callbacks.onPlanConfirm?.(data.confirmId, data.title, data.steps, data.summary); + callbacks.onPlanConfirm?.(data.confirmId, data.title, data.phases, data.steps, data.summary); + }, + + onPhaseProgress: (data: import('../types/api').PhaseProgressEvent) => { + console.log('[DialogSession] onPhaseProgress:', data.phaseId, data.status); + + // 1. 尝试更新 plan segment(兼容旧逻辑) + for (let i = this.segments.length - 1; i >= 0; i--) { + const seg = this.segments[i]; + if (seg.type === 'plan' && seg.planPhases) { + seg.planPhases = seg.planPhases.map(phase => { + if (phase.id === data.phaseId) { + return { ...phase, status: data.status }; + } + return phase; + }); + callbacks.onSegmentUpdate?.(this.segments); + break; + } + } + + // 2. 通知外部更新独立进度条 + callbacks.onPhaseProgress?.(data.phaseId, data.status); + }, + + onPlanStepAdd: (data: import('../types/api').PlanStepAddEvent) => { + console.log('[DialogSession] onPlanStepAdd:', data.phaseId, data.step); + + for (let i = this.segments.length - 1; i >= 0; i--) { + const seg = this.segments[i]; + if (seg.type === 'plan' && seg.planPhases) { + seg.planPhases = seg.planPhases.map(phase => { + if (phase.id === data.phaseId) { + const newSteps = [...(phase.steps || [])]; + if (data.index >= 0 && data.index < newSteps.length) { + newSteps.splice(data.index, 0, data.step); + } else { + newSteps.push(data.step); + } + return { ...phase, steps: newSteps }; + } + return phase; + }); + break; + } + } + callbacks.onSegmentUpdate?.(this.segments); + }, + + onPlanStepRemove: (data: import('../types/api').PlanStepRemoveEvent) => { + console.log('[DialogSession] onPlanStepRemove:', data.phaseId, data.stepIndex); + + for (let i = this.segments.length - 1; i >= 0; i--) { + const seg = this.segments[i]; + if (seg.type === 'plan' && seg.planPhases) { + seg.planPhases = seg.planPhases.map(phase => { + if (phase.id === data.phaseId && phase.steps) { + const newSteps = [...phase.steps]; + newSteps.splice(data.stepIndex, 1); + return { ...phase, steps: newSteps }; + } + return phase; + }); + break; + } + } + callbacks.onSegmentUpdate?.(this.segments); + }, + + onPlanStepUpdate: (data: import('../types/api').PlanStepUpdateEvent) => { + console.log('[DialogSession] onPlanStepUpdate:', data.phaseId, data.stepIndex); + + for (let i = this.segments.length - 1; i >= 0; i--) { + const seg = this.segments[i]; + if (seg.type === 'plan' && seg.planPhases) { + seg.planPhases = seg.planPhases.map(phase => { + if (phase.id === data.phaseId && phase.steps) { + const newSteps = [...phase.steps]; + if (data.stepIndex >= 0 && data.stepIndex < newSteps.length) { + newSteps[data.stepIndex] = data.step; + } + return { ...phase, steps: newSteps }; + } + return phase; + }); + break; + } + } + callbacks.onSegmentUpdate?.(this.segments); + }, + + onPlanSummaryUpdate: (data: import('../types/api').PlanSummaryUpdateEvent) => { + console.log('[DialogSession] onPlanSummaryUpdate'); + + for (let i = this.segments.length - 1; i >= 0; i--) { + const seg = this.segments[i]; + if (seg.type === 'plan') { + seg.planSummary = data.summary; + break; + } + } + callbacks.onSegmentUpdate?.(this.segments); }, onAskUser: async (data: AskUserEvent) => { diff --git a/src/services/sseHandler.ts b/src/services/sseHandler.ts index 4b1dda9..266818f 100644 --- a/src/services/sseHandler.ts +++ b/src/services/sseHandler.ts @@ -45,6 +45,16 @@ export interface SSECallbacks { onToolConfirm?: (data: ToolConfirmEvent) => void; /** 收到计划确认请求(Plan 模式) */ onPlanConfirm?: (data: PlanConfirmEvent) => void; + /** 阶段进度更新 */ + onPhaseProgress?: (data: import('../types/api').PhaseProgressEvent) => void; + /** 添加计划步骤 */ + onPlanStepAdd?: (data: import('../types/api').PlanStepAddEvent) => void; + /** 删除计划步骤 */ + onPlanStepRemove?: (data: import('../types/api').PlanStepRemoveEvent) => void; + /** 更新计划步骤 */ + onPlanStepUpdate?: (data: import('../types/api').PlanStepUpdateEvent) => void; + /** 更新计划摘要 */ + onPlanSummaryUpdate?: (data: import('../types/api').PlanSummaryUpdateEvent) => void; /** 工具开始执行 */ onToolStart?: (data: ToolStartEvent) => void; /** 工具执行完成 */ @@ -289,6 +299,21 @@ function dispatchEvent( case 'plan_confirm': callbacks.onPlanConfirm?.(data as PlanConfirmEvent); break; + case 'phase_progress': + callbacks.onPhaseProgress?.(data as import('../types/api').PhaseProgressEvent); + break; + case 'plan_step_add': + callbacks.onPlanStepAdd?.(data as import('../types/api').PlanStepAddEvent); + break; + case 'plan_step_remove': + callbacks.onPlanStepRemove?.(data as import('../types/api').PlanStepRemoveEvent); + break; + case 'plan_step_update': + callbacks.onPlanStepUpdate?.(data as import('../types/api').PlanStepUpdateEvent); + break; + case 'plan_summary_update': + callbacks.onPlanSummaryUpdate?.(data as import('../types/api').PlanSummaryUpdateEvent); + break; case 'tool_start': callbacks.onToolStart?.(data as ToolStartEvent); break; diff --git a/src/types/api.ts b/src/types/api.ts index 2265be3..53d85f5 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -56,6 +56,11 @@ export type SSEEventType = | "tool_call" // 客户端工具调用请求 | "tool_confirm" // 工具确认请求(Ask 模式) | "plan_confirm" // 计划确认请求(Plan 模式) + | "phase_progress" // 阶段进度更新 + | "plan_step_add" // 添加计划步骤 + | "plan_step_remove" // 删除计划步骤 + | "plan_step_update" // 更新计划步骤 + | "plan_summary_update" // 更新计划摘要 | "tool_start" // 工具开始执行 | "tool_complete" // 工具执行完成 | "tool_error" // 工具执行错误 @@ -109,20 +114,83 @@ export interface ToolConfirmEvent { timestamp: number; } +/** 计划步骤 */ +export interface PlanStep { + /** 步骤名称 */ + name: string; + /** 步骤描述 */ + description?: string; +} + +/** 计划阶段 */ +export interface PlanPhase { + /** 阶段ID: spec/design/sim/done */ + id: string; + /** 阶段名称 */ + name: string; + /** 阶段状态: skipped/completed/current/pending */ + status: string; + /** 跳过原因 */ + reason?: string; + /** 阶段内的步骤 */ + steps: PlanStep[]; +} + /** plan_confirm 事件数据(Plan 模式计划确认) */ export interface PlanConfirmEvent { /** 确认ID */ confirmId: number; /** 计划标题 */ title: string; - /** 执行步骤列表 */ - steps: string[]; + /** 四阶段计划列表(新格式) */ + phases?: PlanPhase[]; + /** 执行步骤列表(旧格式,兼容) */ + steps?: string[]; /** 计划摘要 */ summary: string; /** 时间戳 */ timestamp: number; } +/** phase_progress 事件数据(阶段进度更新) */ +export interface PhaseProgressEvent { + /** 阶段ID: spec/design/sim/done */ + phaseId: string; + /** 状态: current/completed */ + status: string; + /** 时间戳 */ + timestamp: number; +} + +/** plan_step_add 事件数据(添加计划步骤) */ +export interface PlanStepAddEvent { + phaseId: string; + step: PlanStep; + index: number; + timestamp: number; +} + +/** plan_step_remove 事件数据(删除计划步骤) */ +export interface PlanStepRemoveEvent { + phaseId: string; + stepIndex: number; + timestamp: number; +} + +/** plan_step_update 事件数据(更新计划步骤) */ +export interface PlanStepUpdateEvent { + phaseId: string; + stepIndex: number; + step: PlanStep; + timestamp: number; +} + +/** plan_summary_update 事件数据(更新计划摘要) */ +export interface PlanSummaryUpdateEvent { + summary: string; + timestamp: number; +} + /** ask_user 事件数据 */ export interface AskUserEvent { askId: string; diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index 872e043..4f34c90 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -288,6 +288,36 @@ async function handleUserMessageWithBackend( percentage: data.percentage, }); }, + + onPhaseProgress: (phaseId, status) => { + // 发送阶段进度更新到 WebView + // 映射 phaseId: sim -> simulation + const stepMap: Record = { + spec: "spec", + design: "design", + sim: "simulation", + done: "done", + }; + const step = stepMap[phaseId] || phaseId; + + if (status === "current") { + // 显示进度条并更新到当前步骤 + panel.webview.postMessage({ type: "showProgress" }); + panel.webview.postMessage({ type: "updateProgress", step }); + } else if (status === "completed") { + // 更新到下一步(或完成) + const steps = ["spec", "design", "simulation", "done"]; + const currentIndex = steps.indexOf(step); + if (currentIndex < steps.length - 1) { + panel.webview.postMessage({ + type: "updateProgress", + step: steps[currentIndex + 1], + }); + } else { + panel.webview.postMessage({ type: "completeProgress" }); + } + } + }, }, mode, serviceTier // 传递服务等级 diff --git a/src/views/planCard.ts b/src/views/planCard.ts index bb8d3f2..3aea4ff 100644 --- a/src/views/planCard.ts +++ b/src/views/planCard.ts @@ -211,6 +211,168 @@ export function getPlanCardStyles(): string { .plan-actions .custom-submit:hover { background: var(--vscode-button-hoverBackground); } + + /* 阶段进度条样式 */ + .phase-progress { + display: flex; + align-items: center; + padding: 12px 16px; + background: var(--vscode-sideBar-background); + border-bottom: 1px solid var(--vscode-input-border); + } + .phase-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--vscode-descriptionForeground); + } + .phase-item.current { + color: var(--vscode-textLink-foreground); + font-weight: 600; + } + .phase-item.completed { + color: #4caf50; + } + .phase-item.skipped { + color: var(--vscode-descriptionForeground); + opacity: 0.6; + } + .phase-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--vscode-input-border); + flex-shrink: 0; + } + .phase-dot.current { + background: var(--vscode-textLink-foreground); + box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.2); + } + .phase-dot.completed { + background: #4caf50; + } + .phase-dot.skipped { + background: var(--vscode-descriptionForeground); + opacity: 0.5; + } + .phase-line { + flex: 1; + height: 2px; + background: var(--vscode-input-border); + margin: 0 8px; + } + .phase-line.completed { + background: #4caf50; + } + + /* 阶段列表样式 */ + .plan-phases { + font-size: 13px; + } + .plan-phase { + margin-bottom: 12px; + border: 1px solid var(--vscode-input-border); + border-radius: 6px; + overflow: hidden; + } + .plan-phase:last-child { + margin-bottom: 0; + } + .phase-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + background: var(--vscode-list-hoverBackground); + cursor: pointer; + user-select: none; + } + .phase-header:hover { + background: var(--vscode-list-activeSelectionBackground); + } + .phase-toggle { + font-size: 10px; + color: var(--vscode-descriptionForeground); + transition: transform 0.2s; + } + .phase-toggle.expanded { + transform: rotate(90deg); + } + .phase-name { + flex: 1; + font-weight: 500; + } + .phase-status { + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + background: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + } + .phase-status.current { + background: var(--vscode-textLink-foreground); + color: white; + } + .phase-status.skipped { + background: var(--vscode-descriptionForeground); + opacity: 0.6; + } + .phase-status.completed { + background: #4caf50; + color: white; + } + .phase-content { + padding: 0 12px; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease, padding 0.3s ease; + } + .phase-content.expanded { + padding: 12px; + max-height: 500px; + } + .phase-reason { + font-size: 12px; + color: var(--vscode-descriptionForeground); + font-style: italic; + margin-bottom: 8px; + } + .phase-steps { + margin: 0; + padding: 0; + list-style: none; + } + .phase-step-item { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 6px 0; + border-bottom: 1px solid var(--vscode-input-border); + } + .phase-step-item:last-child { + border-bottom: none; + } + .phase-step-checkbox { + width: 14px; + height: 14px; + border: 2px solid var(--vscode-textLink-foreground); + border-radius: 3px; + flex-shrink: 0; + margin-top: 2px; + } + .phase-step-text { + flex: 1; + } + .phase-step-name { + font-weight: 500; + color: var(--vscode-foreground); + } + .phase-step-desc { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin-top: 2px; + } `; } @@ -330,6 +492,89 @@ export function getPlanCardScript(): string { }).join(''); } + // 渲染阶段进度条 + function renderPhaseProgress(phases) { + if (!phases || phases.length === 0) return ''; + + const phaseNames = { spec: 'Spec', design: 'Design', sim: 'Sim', done: 'Done' }; + let html = '
'; + + phases.forEach((phase, i) => { + const name = phaseNames[phase.id] || phase.name || phase.id; + const status = phase.status || 'pending'; + + html += \`
+ + \${name} +
\`; + + // 添加连接线(最后一个不加) + if (i < phases.length - 1) { + const lineStatus = (status === 'completed' || status === 'skipped') ? 'completed' : ''; + html += \`
\`; + } + }); + + html += '
'; + return html; + } + + // 渲染阶段列表(两级结构) + function renderPlanPhases(phases) { + if (!phases || phases.length === 0) return ''; + + const statusLabels = { + skipped: '跳过', + completed: '已完成', + current: '当前', + pending: '待执行' + }; + + return phases.map((phase, i) => { + const status = phase.status || 'pending'; + const statusLabel = statusLabels[status] || status; + const isExpanded = status === 'current'; + const hasSteps = phase.steps && phase.steps.length > 0; + const hasReason = phase.reason && status === 'skipped'; + + let stepsHtml = ''; + if (phase.steps && phase.steps.length > 0) { + stepsHtml = phase.steps.map(step => \` +
  • + +
    +
    \${step.name || ''}
    + \${step.description ? \`
    \${step.description}
    \` : ''} +
    +
  • + \`).join(''); + } + + return \` +
    +
    + + \${phase.name || phase.id} + \${statusLabel} +
    +
    + \${hasReason ? \`
    \${phase.reason}
    \` : ''} + \${hasSteps ? \`
      \${stepsHtml}
    \` : ''} + \${!hasSteps && !hasReason ? '
    暂无步骤
    ' : ''} +
    +
    + \`; + }).join(''); + } + + // 切换阶段展开/折叠 + function togglePhase(header) { + const toggle = header.querySelector('.phase-toggle'); + const content = header.nextElementSibling; + toggle.classList.toggle('expanded'); + content.classList.toggle('expanded'); + } + // 渲染计划卡片(在 updateSegmentsRealtime 中使用) function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) { segmentDiv.className += ' segment-plan'; @@ -342,8 +587,15 @@ export function getPlanCardScript(): string { segmentDiv.classList.add('answered'); } - // 解析并渲染步骤 - const stepsHtml = renderPlanSteps(segment.planSteps || []); + // 判断是否有 phases(新格式)还是 steps(旧格式) + const hasPhases = segment.planPhases && segment.planPhases.length > 0; + + // 渲染阶段进度条和阶段列表(新格式) + const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : ''; + const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : ''; + + // 兼容旧格式:渲染步骤列表 + const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : ''; // 选项按钮 const options = ['确认执行', '修改计划', '取消']; @@ -361,9 +613,10 @@ export function getPlanCardScript(): string { ${plannerIconSvg} \${segment.planTitle || '执行计划'} + \${progressHtml}
    \${summaryHtml}
    -
    \${stepsHtml}
    + \${hasPhases ? \`
    \${phasesHtml}
    \` : \`
    \${stepsHtml}
    \`}
    \${optionsHtml}
    @@ -424,7 +677,16 @@ export function getPlanCardScript(): string { // 渲染计划卡片(在 renderSegments 中使用) function renderPlanCardStatic(segment, segmentDiv) { segmentDiv.className += ' segment-plan'; - const stepsHtml = renderPlanSteps(segment.planSteps || []); + + // 判断是否有 phases(新格式)还是 steps(旧格式) + const hasPhases = segment.planPhases && segment.planPhases.length > 0; + + // 渲染阶段进度条和阶段列表(新格式) + const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : ''; + const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : ''; + + // 兼容旧格式:渲染步骤列表 + const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : ''; // 渲染 Markdown 格式的摘要 const summaryHtml = renderPlanMarkdown(segment.planSummary || ''); @@ -435,9 +697,10 @@ export function getPlanCardScript(): string { 📋 \${segment.planTitle || '执行计划'}
    + \${progressHtml}
    \${summaryHtml}
    -
    \${stepsHtml}
    + \${hasPhases ? \`
    \${phasesHtml}
    \` : \`
    \${stepsHtml}
    \`}