From 47546a9d15953a14511127dc4b0288c3f341af43 Mon Sep 17 00:00:00 2001 From: Fam Zheng Date: Sat, 7 Mar 2026 16:41:15 +0000 Subject: [PATCH] feat: add approve/reject buttons for wait_for_approval - CommentSection shows explicit approve/reject buttons when waiting - Reject aborts the workflow, approve continues with optional feedback - Backend parses approved:/rejected: prefixes from comment content --- src/agent.rs | 29 ++++++++++- web/src/components/CommentSection.vue | 73 +++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index 5bec0c9..216e2d3 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -1177,7 +1177,27 @@ async fn run_agent_loop( } }; - tracing::info!("[workflow {}] Approval received: {}", workflow_id, approval_content); + tracing::info!("[workflow {}] Approval response: {}", workflow_id, approval_content); + + // Check if user rejected + if approval_content.starts_with("rejected:") { + let reason = approval_content.strip_prefix("rejected:").unwrap_or("").trim(); + tracing::info!("[workflow {}] User rejected: {}", workflow_id, reason); + if let Some(step) = state.steps.iter_mut().find(|s| s.order == cur) { + step.status = StepStatus::Failed; + step.user_feedbacks.push(format!("用户终止: {}", reason)); + } + log_execution(pool, broadcast_tx, workflow_id, cur, "wait_for_approval", "rejected", reason, "failed").await; + // Return error to end the agent loop; caller sets workflow to "failed" + return Err(anyhow::anyhow!("用户终止了执行: {}", reason)); + } + + // Approved — extract feedback after "approved:" prefix if present + let feedback = if approval_content.starts_with("approved:") { + approval_content.strip_prefix("approved:").unwrap_or("").trim().to_string() + } else { + approval_content.clone() + }; // Resume: restore Running status if let Some(step) = state.steps.iter_mut().find(|s| s.order == cur) { @@ -1197,8 +1217,13 @@ async fn run_agent_loop( steps: plan_infos_from_state(&state), }); + let tool_msg = if feedback.is_empty() { + "用户已确认,继续执行。".to_string() + } else { + format!("用户已确认。反馈: {}", feedback) + }; state.current_step_chat_history.push( - ChatMessage::tool_result(&tc.id, &format!("用户已确认。反馈: {}", approval_content)) + ChatMessage::tool_result(&tc.id, &tool_msg) ); } diff --git a/web/src/components/CommentSection.vue b/web/src/components/CommentSection.vue index df63634..0af5948 100644 --- a/web/src/components/CommentSection.vue +++ b/web/src/components/CommentSection.vue @@ -15,12 +15,8 @@ const emit = defineEmits<{ const input = ref('') const textareaRef = ref(null) -function submit() { - if (props.disabled) return +function buildText(): string { const text = input.value.trim() - if (!text && !props.quotes.length) return - - // Build final text: quotes as block references, then user text let final = '' for (const q of props.quotes) { final += `> ${q}\n` @@ -29,8 +25,27 @@ function submit() { final += '\n' } final += text + return final.trim() +} - emit('submit', final.trim()) +function submit() { + if (props.disabled) return + const text = input.value.trim() + if (!text && !props.quotes.length) return + + emit('submit', buildText()) + input.value = '' +} + +function approve() { + const feedback = buildText() + emit('submit', feedback ? `approved: ${feedback}` : 'approved:') + input.value = '' +} + +function reject() { + const feedback = buildText() + emit('submit', feedback ? `rejected: ${feedback}` : 'rejected: 用户终止') input.value = '' } @@ -53,7 +68,7 @@ defineExpose({ focusInput })