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:
@@ -1124,7 +1124,7 @@ async fn process_feedback(
|
|||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
// Apply docker-cache diff
|
// Apply docker-cache diff
|
||||||
state.apply_plan_diff(new_steps);
|
let diff = state.apply_plan_diff(new_steps);
|
||||||
|
|
||||||
// Broadcast updated plan
|
// Broadcast updated plan
|
||||||
let _ = broadcast_tx.send(WsMessage::PlanUpdate {
|
let _ = broadcast_tx.send(WsMessage::PlanUpdate {
|
||||||
@@ -1134,6 +1134,10 @@ async fn process_feedback(
|
|||||||
|
|
||||||
tracing::info!("[workflow {}] Plan revised via feedback. First actionable: {:?}",
|
tracing::info!("[workflow {}] Plan revised via feedback. First actionable: {:?}",
|
||||||
workflow_id, state.first_actionable_step());
|
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 {
|
} else {
|
||||||
|
|||||||
66
src/state.rs
66
src/state.rs
@@ -130,7 +130,23 @@ impl AgentState {
|
|||||||
/// Docker-build-cache 风格的 plan diff。
|
/// Docker-build-cache 风格的 plan diff。
|
||||||
/// 比较 (title, description),user_feedbacks 不参与比较。
|
/// 比较 (title, description),user_feedbacks 不参与比较。
|
||||||
/// 第一个 mismatch 开始,该步骤及后续全部 invalidate → Pending。
|
/// 第一个 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 old = &self.steps;
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut invalidated = false;
|
let mut invalidated = false;
|
||||||
@@ -158,6 +174,9 @@ impl AgentState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.steps = result;
|
self.steps = result;
|
||||||
|
|
||||||
|
// Generate unified diff
|
||||||
|
diff_strings(&old_yaml, &new_yaml)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 找到第一个需要执行的步骤 (Pending 或 Running)。
|
/// 找到第一个需要执行的步骤 (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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -131,6 +131,25 @@ function formatLatency(ms: number): string {
|
|||||||
return ms + 'ms'
|
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 }[] {
|
function parseToolCalls(json: string): { name: string; arguments_preview: string }[] {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(json)
|
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">
|
<div v-if="item.entry.tool_input && item.entry.tool_name !== 'text_response'" class="exec-command">
|
||||||
<code>{{ item.entry.tool_input }}</code>
|
<code>{{ item.entry.tool_input }}</code>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -665,4 +684,18 @@ watch(logItems, () => {
|
|||||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
font-size: 10px;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user