diff --git a/src/agent.rs b/src/agent.rs index 3114133..98688c7 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -36,6 +36,7 @@ pub enum WsMessage { ReportReady { workflow_id: String }, ProjectUpdate { project_id: String, name: String }, LlmCallLog { workflow_id: String, entry: crate::db::LlmCallLogEntry }, + ActivityUpdate { workflow_id: String, activity: String }, Error { message: String }, } @@ -1109,6 +1110,10 @@ async fn run_step_loop( let phase_label = format!("step({})", step_order); tracing::info!("[workflow {}] Step {} LLM call #{} msgs={}", workflow_id, step_order, iteration + 1, messages.len()); + let _ = broadcast_tx.send(WsMessage::ActivityUpdate { + workflow_id: workflow_id.to_string(), + activity: format!("步骤 {} — 等待 LLM 响应...", step_order), + }); let call_start = std::time::Instant::now(); let response = match llm.chat_with_tools(messages, &step_tools).await { Ok(r) => r, @@ -1185,6 +1190,10 @@ async fn run_step_loop( "wait_for_approval" => { let reason = args["reason"].as_str().unwrap_or("等待确认"); + let _ = broadcast_tx.send(WsMessage::ActivityUpdate { + workflow_id: workflow_id.to_string(), + activity: format!("步骤 {} — 等待用户确认: {}", step_order, reason), + }); // Broadcast waiting status let _ = broadcast_tx.send(WsMessage::PlanUpdate { @@ -1350,6 +1359,10 @@ async fn run_step_loop( // External tools name if external_tools.as_ref().is_some_and(|e| e.has_tool(name)) => { + let _ = broadcast_tx.send(WsMessage::ActivityUpdate { + workflow_id: workflow_id.to_string(), + activity: format!("步骤 {} — 工具: {}", step_order, name), + }); let result = match external_tools.unwrap().invoke(name, &tc.function.arguments, workdir).await { Ok(output) => { let truncated = truncate_str(&output, 8192); @@ -1364,6 +1377,20 @@ async fn run_step_loop( // IO tools: execute, read_file, write_file, list_files _ => { + let tool_desc = match tc.function.name.as_str() { + "execute" => { + let cmd_preview = args.get("command").and_then(|v| v.as_str()).unwrap_or("").chars().take(60).collect::(); + format!("执行命令: {}", cmd_preview) + } + "read_file" => format!("读取文件: {}", args.get("path").and_then(|v| v.as_str()).unwrap_or("?")), + "write_file" => format!("写入文件: {}", args.get("path").and_then(|v| v.as_str()).unwrap_or("?")), + "list_files" => "列出文件".to_string(), + other => format!("工具: {}", other), + }; + let _ = broadcast_tx.send(WsMessage::ActivityUpdate { + workflow_id: workflow_id.to_string(), + activity: format!("步骤 {} — {}", step_order, tool_desc), + }); let result = execute_tool(&tc.function.name, &tc.function.arguments, workdir, exec).await; let status = if result.starts_with("Error:") { "failed" } else { "done" }; log_execution(pool, broadcast_tx, workflow_id, step_order, &tc.function.name, &tc.function.arguments, &result, status).await; @@ -1492,6 +1519,10 @@ async fn run_agent_loop( let tool_count = planning_tools.len() as i32; tracing::info!("[workflow {}] Planning LLM call #{} msgs={}", workflow_id, iteration + 1, messages.len()); + let _ = broadcast_tx.send(WsMessage::ActivityUpdate { + workflow_id: workflow_id.to_string(), + activity: "规划中 — 等待 LLM 响应...".to_string(), + }); let call_start = std::time::Instant::now(); let response = match llm.chat_with_tools(messages, &planning_tools).await { Ok(r) => r, @@ -1702,6 +1733,10 @@ async fn run_agent_loop( state.current_step_chat_history.clear(); tracing::info!("[workflow {}] Coordinator review for step {}", workflow_id, step_order); + let _ = broadcast_tx.send(WsMessage::ActivityUpdate { + workflow_id: workflow_id.to_string(), + activity: format!("步骤 {} 完成 — 协调器审核中...", step_order), + }); let call_start = std::time::Instant::now(); let coord_response = match llm.chat_with_tools(coord_messages.clone(), &coordinator_tools).await { Ok(r) => r, diff --git a/web/src/components/ExecutionSection.vue b/web/src/components/ExecutionSection.vue index 923e99f..48b5a0e 100644 --- a/web/src/components/ExecutionSection.vue +++ b/web/src/components/ExecutionSection.vue @@ -12,6 +12,7 @@ const props = defineProps<{ createdAt: string workflowStatus: string workflowId: string + currentActivity: string }>() const emit = defineEmits<{ @@ -278,6 +279,10 @@ watch(logItems, () => { +
+ + {{ currentActivity }} +
提交需求后,日志将显示在这里
@@ -529,6 +534,36 @@ watch(logItems, () => { margin: 0; } +.activity-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + border-radius: 6px; + background: rgba(79, 195, 247, 0.08); + border: 1px dashed var(--accent); + font-size: 13px; + color: var(--accent); +} + +.activity-spinner { + width: 14px; + height: 14px; + border: 2px solid var(--accent); + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; + flex-shrink: 0; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.activity-text { + font-weight: 500; +} + .empty-state { color: var(--text-secondary); font-size: 13px; diff --git a/web/src/components/WorkflowView.vue b/web/src/components/WorkflowView.vue index 2acea9f..e1b159c 100644 --- a/web/src/components/WorkflowView.vue +++ b/web/src/components/WorkflowView.vue @@ -24,6 +24,7 @@ const planSteps = ref([]) const comments = ref([]) const llmCalls = ref([]) const quotes = ref([]) +const currentActivity = ref('') const error = ref('') const rightTab = ref<'log' | 'timers'>('log') const commentRef = ref | null>(null) @@ -92,6 +93,14 @@ function handleWsMessage(msg: WsMessage) { case 'WorkflowStatusUpdate': if (workflow.value && msg.workflow_id === workflow.value.id) { workflow.value = { ...workflow.value, status: msg.status as any } + if (msg.status === 'done' || msg.status === 'failed') { + currentActivity.value = '' + } + } + break + case 'ActivityUpdate': + if (workflow.value && msg.workflow_id === workflow.value.id) { + currentActivity.value = msg.activity } break case 'RequirementUpdate': @@ -186,6 +195,7 @@ async function onSubmitComment(text: string) { :createdAt="workflow?.created_at || ''" :workflowStatus="workflow?.status || 'pending'" :workflowId="workflow?.id || ''" + :currentActivity="currentActivity" @quote="addQuote" /> void