xtalforge_agent/changes.rs
1//! Change tracking (Task 8.1.4).
2//!
3//! The document keeps one [`CommandRecord`] per edit with its
4//! [`xtalforge_commands::Origin`]. This module groups consecutive
5//! edits made through the agent into bursts, so a user can review the
6//! assistant's work and undo it as a unit.
7//!
8//! Ported from Crystal-Builder `xtal/agent/changes.py` (MIT).
9
10use serde::{Deserialize, Serialize};
11use xtalforge_commands::{CommandRecord, Origin};
12
13/// A consecutive run of edits made through the agent.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct ChangeBurst {
16 /// Position of the first edit in the document history, zero based.
17 pub start: usize,
18 /// Number of edits in the burst.
19 pub len: usize,
20 /// One description per edit, in application order.
21 pub descriptions: Vec<String>,
22 /// Number of `undo` calls that remove the whole burst when it is the
23 /// most recent history entry; `None` when later user edits exist.
24 pub undo_steps: Option<usize>,
25}
26
27/// Groups the agent-origin runs of a command log.
28///
29/// A run is consecutive records whose origin is [`xtalforge_commands::Origin::Agent`]; a user or
30/// script edit between two agent edits starts a new burst. The log only holds
31/// entries that were not trimmed from the undo stack.
32#[must_use]
33pub fn changes_for(history: &[CommandRecord]) -> Vec<ChangeBurst> {
34 let mut bursts = Vec::new();
35 let mut index = 0;
36 while let Some(record) = history.get(index) {
37 if record.origin != Origin::Agent {
38 index += 1;
39 continue;
40 }
41 let start = index;
42 while let Some(record) = history.get(index) {
43 if record.origin != Origin::Agent {
44 break;
45 }
46 index += 1;
47 }
48 let Some(run) = history.get(start..index) else {
49 break;
50 };
51 let len = index - start;
52 let descriptions = run
53 .iter()
54 .map(|record| record.description.clone())
55 .collect();
56 let at_end = index == history.len();
57 bursts.push(ChangeBurst {
58 start,
59 len,
60 descriptions,
61 undo_steps: if at_end { Some(len) } else { None },
62 });
63 }
64 bursts
65}
66
67/// Number of edits in the log made through the agent.
68#[must_use]
69pub fn agent_edit_count(history: &[CommandRecord]) -> usize {
70 history
71 .iter()
72 .filter(|record| record.origin == Origin::Agent)
73 .count()
74}