feat: show plan diff in execution log when revise_plan is called
- apply_plan_diff now returns a YAML unified diff string - Pure Rust LCS diff implementation (no external dependency) - revise_plan logs the diff to execution log with ```diff fencing - Frontend renders diff with green/red syntax highlighting
This commit is contained in:
parent
978af45d5f
commit
63bbbae17c
@ -1124,7 +1124,7 @@ async fn process_feedback(
|
||||
}).collect();
|
||||
|
||||
// Apply docker-cache diff
|
||||
state.apply_plan_diff(new_steps);
|
||||
let diff = state.apply_plan_diff(new_steps);
|
||||
|
||||
// Broadcast updated plan
|
||||
let _ = broadcast_tx.send(WsMessage::PlanUpdate {
|
||||
@ -1134,6 +1134,10 @@ async fn process_feedback(
|
||||
|
||||
tracing::info!("[workflow {}] Plan revised via feedback. First actionable: {:?}",
|
||||
workflow_id, state.first_actionable_step());
|
||||
|
||||
// Log the diff so frontend can show what changed
|
||||
let diff_display = format!("```diff\n{}\n```", diff);
|
||||
log_execution(pool, broadcast_tx, workflow_id, 0, "revise_plan", "计划变更", &diff_display, "done").await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
66
src/state.rs
66
src/state.rs
@ -130,7 +130,23 @@ impl AgentState {
|
||||
/// Docker-build-cache 风格的 plan diff。
|
||||
/// 比较 (title, description),user_feedbacks 不参与比较。
|
||||
/// 第一个 mismatch 开始,该步骤及后续全部 invalidate → Pending。
|
||||
pub fn apply_plan_diff(&mut self, new_steps: Vec<Step>) {
|
||||
/// Apply docker-cache style diff. Returns a unified-diff string (YAML format)
|
||||
/// showing what changed, for logging in the frontend.
|
||||
pub fn apply_plan_diff(&mut self, new_steps: Vec<Step>) -> String {
|
||||
// Serialize old/new plans to YAML for diff (only title + description)
|
||||
let to_yaml = |steps: &[Step]| -> String {
|
||||
let items: Vec<serde_json::Value> = steps.iter().map(|s| {
|
||||
serde_json::json!({
|
||||
"step": s.order,
|
||||
"title": s.title,
|
||||
"description": s.description,
|
||||
})
|
||||
}).collect();
|
||||
serde_yaml::to_string(&items).unwrap_or_default()
|
||||
};
|
||||
let old_yaml = to_yaml(&self.steps);
|
||||
let new_yaml = to_yaml(&new_steps);
|
||||
|
||||
let old = &self.steps;
|
||||
let mut result = Vec::new();
|
||||
let mut invalidated = false;
|
||||
@ -158,6 +174,9 @@ impl AgentState {
|
||||
}
|
||||
|
||||
self.steps = result;
|
||||
|
||||
// Generate unified diff
|
||||
diff_strings(&old_yaml, &new_yaml)
|
||||
}
|
||||
|
||||
/// 找到第一个需要执行的步骤 (Pending 或 Running)。
|
||||
@ -255,6 +274,51 @@ impl AgentState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple line-by-line unified diff (no external dependency).
|
||||
/// Uses longest common subsequence to produce a clean diff.
|
||||
fn diff_strings(old: &str, new: &str) -> String {
|
||||
let old_lines: Vec<&str> = old.lines().collect();
|
||||
let new_lines: Vec<&str> = new.lines().collect();
|
||||
|
||||
if old_lines == new_lines {
|
||||
return String::from("(no changes)");
|
||||
}
|
||||
|
||||
// LCS table
|
||||
let m = old_lines.len();
|
||||
let n = new_lines.len();
|
||||
let mut dp = vec![vec![0u32; n + 1]; m + 1];
|
||||
for i in 1..=m {
|
||||
for j in 1..=n {
|
||||
dp[i][j] = if old_lines[i - 1] == new_lines[j - 1] {
|
||||
dp[i - 1][j - 1] + 1
|
||||
} else {
|
||||
dp[i - 1][j].max(dp[i][j - 1])
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Backtrack to produce diff lines
|
||||
let mut result = Vec::new();
|
||||
let (mut i, mut j) = (m, n);
|
||||
while i > 0 || j > 0 {
|
||||
if i > 0 && j > 0 && old_lines[i - 1] == new_lines[j - 1] {
|
||||
result.push(format!(" {}", old_lines[i - 1]));
|
||||
i -= 1;
|
||||
j -= 1;
|
||||
} else if j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j]) {
|
||||
result.push(format!("+{}", new_lines[j - 1]));
|
||||
j -= 1;
|
||||
} else {
|
||||
result.push(format!("-{}", old_lines[i - 1]));
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
result.reverse();
|
||||
result.join("\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@ -131,6 +131,25 @@ function formatLatency(ms: number): string {
|
||||
return ms + 'ms'
|
||||
}
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
||||
function isDiffOutput(output: string): boolean {
|
||||
return output.startsWith('```diff\n')
|
||||
}
|
||||
|
||||
function renderDiff(output: string): string {
|
||||
// Strip ```diff fences
|
||||
const inner = output.replace(/^```diff\n/, '').replace(/\n```$/, '')
|
||||
return inner.split('\n').map(line => {
|
||||
const esc = escapeHtml(line)
|
||||
if (line.startsWith('+')) return `<span class="diff-add">${esc}</span>`
|
||||
if (line.startsWith('-')) return `<span class="diff-del">${esc}</span>`
|
||||
return esc
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
function parseToolCalls(json: string): { name: string; arguments_preview: string }[] {
|
||||
try {
|
||||
return JSON.parse(json)
|
||||
@ -275,7 +294,7 @@ watch(logItems, () => {
|
||||
<div v-if="item.entry.tool_input && item.entry.tool_name !== 'text_response'" class="exec-command">
|
||||
<code>{{ item.entry.tool_input }}</code>
|
||||
</div>
|
||||
<pre v-if="item.entry.output">{{ item.entry.output }}</pre>
|
||||
<pre v-if="item.entry.output" :class="{ 'diff-output': isDiffOutput(item.entry.output) }" v-html="isDiffOutput(item.entry.output) ? renderDiff(item.entry.output) : escapeHtml(item.entry.output)"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -665,4 +684,18 @@ watch(logItems, () => {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
pre.diff-output .diff-add {
|
||||
color: #22c55e;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
pre.diff-output .diff-del {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user