Skip to main content

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}