zeta2: Include a single unified diff for the edit history (#40400)

Agus Zubiaga and Oleksiy Syvokon created

Instead of producing multiple code blocks for each edit history event,
we now produce a continuous unified diff.

Release Notes:

- N/A

---------

Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>

Change summary

Cargo.lock                                          |   1 
crates/cloud_llm_client/Cargo.toml                  |   1 
crates/cloud_llm_client/src/predict_edits_v3.rs     | 112 +++++++++++++++
crates/cloud_zeta2_prompt/src/cloud_zeta2_prompt.rs |  55 +------
4 files changed, 123 insertions(+), 46 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3384,6 +3384,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "chrono",
+ "indoc",
  "pretty_assertions",
  "serde",
  "serde_json",

crates/cloud_llm_client/src/predict_edits_v3.rs 🔗

@@ -1,6 +1,7 @@
 use chrono::Duration;
 use serde::{Deserialize, Serialize};
 use std::{
+    fmt::Display,
     ops::{Add, Range, Sub},
     path::{Path, PathBuf},
     sync::Arc,
@@ -91,6 +92,38 @@ pub enum Event {
     },
 }
 
+impl Display for Event {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Event::BufferChange {
+                path,
+                old_path,
+                diff,
+                predicted,
+            } => {
+                let new_path = path.as_deref().unwrap_or(Path::new("untitled"));
+                let old_path = old_path.as_deref().unwrap_or(new_path);
+
+                if *predicted {
+                    write!(
+                        f,
+                        "// User accepted prediction:\n--- a/{}\n+++ b/{}\n{diff}",
+                        old_path.display(),
+                        new_path.display()
+                    )
+                } else {
+                    write!(
+                        f,
+                        "--- a/{}\n+++ b/{}\n{diff}",
+                        old_path.display(),
+                        new_path.display()
+                    )
+                }
+            }
+        }
+    }
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Signature {
     pub text: String,
@@ -204,3 +237,82 @@ impl Sub for Line {
         Self(self.0 - rhs.0)
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use indoc::indoc;
+    use pretty_assertions::assert_eq;
+
+    #[test]
+    fn test_event_display() {
+        let ev = Event::BufferChange {
+            path: None,
+            old_path: None,
+            diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
+            predicted: false,
+        };
+        assert_eq!(
+            ev.to_string(),
+            indoc! {"
+                --- a/untitled
+                +++ b/untitled
+                @@ -1,2 +1,2 @@
+                -a
+                -b
+            "}
+        );
+
+        let ev = Event::BufferChange {
+            path: Some(PathBuf::from("foo/bar.txt")),
+            old_path: Some(PathBuf::from("foo/bar.txt")),
+            diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
+            predicted: false,
+        };
+        assert_eq!(
+            ev.to_string(),
+            indoc! {"
+                --- a/foo/bar.txt
+                +++ b/foo/bar.txt
+                @@ -1,2 +1,2 @@
+                -a
+                -b
+            "}
+        );
+
+        let ev = Event::BufferChange {
+            path: Some(PathBuf::from("abc.txt")),
+            old_path: Some(PathBuf::from("123.txt")),
+            diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
+            predicted: false,
+        };
+        assert_eq!(
+            ev.to_string(),
+            indoc! {"
+                --- a/123.txt
+                +++ b/abc.txt
+                @@ -1,2 +1,2 @@
+                -a
+                -b
+            "}
+        );
+
+        let ev = Event::BufferChange {
+            path: Some(PathBuf::from("abc.txt")),
+            old_path: Some(PathBuf::from("123.txt")),
+            diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
+            predicted: true,
+        };
+        assert_eq!(
+            ev.to_string(),
+            indoc! {"
+                // User accepted prediction:
+                --- a/123.txt
+                +++ b/abc.txt
+                @@ -1,2 +1,2 @@
+                -a
+                -b
+            "}
+        );
+    }
+}

crates/cloud_zeta2_prompt/src/cloud_zeta2_prompt.rs 🔗

@@ -1,9 +1,7 @@
 //! Zeta2 prompt planning and generation code shared with cloud.
 
 use anyhow::{Context as _, Result, anyhow};
-use cloud_llm_client::predict_edits_v3::{
-    self, Event, Line, Point, PromptFormat, ReferencedDeclaration,
-};
+use cloud_llm_client::predict_edits_v3::{self, Line, Point, PromptFormat, ReferencedDeclaration};
 use indoc::indoc;
 use ordered_float::OrderedFloat;
 use rustc_hash::{FxHashMap, FxHashSet};
@@ -419,7 +417,7 @@ impl<'a> PlannedPrompt<'a> {
         };
 
         if self.request.events.is_empty() {
-            prompt.push_str("No edits yet.\n\n");
+            prompt.push_str("(No edit history)\n\n");
         } else {
             prompt.push_str(
                 "The following are the latest edits made by the user, from earlier to later.\n\n",
@@ -465,50 +463,15 @@ impl<'a> PlannedPrompt<'a> {
     }
 
     fn push_events(output: &mut String, events: &[predict_edits_v3::Event]) {
-        for event in events {
-            match event {
-                Event::BufferChange {
-                    path,
-                    old_path,
-                    diff,
-                    predicted,
-                } => {
-                    if let Some(old_path) = &old_path
-                        && let Some(new_path) = &path
-                    {
-                        if old_path != new_path {
-                            writeln!(
-                                output,
-                                "User renamed {} to {}\n\n",
-                                old_path.display(),
-                                new_path.display()
-                            )
-                            .unwrap();
-                        }
-                    }
+        if events.is_empty() {
+            return;
+        };
 
-                    let path = path
-                        .as_ref()
-                        .map_or_else(|| "untitled".to_string(), |path| path.display().to_string());
-
-                    if *predicted {
-                        writeln!(
-                            output,
-                            "User accepted prediction {:?}:\n`````diff\n{}\n`````\n",
-                            path, diff
-                        )
-                        .unwrap();
-                    } else {
-                        writeln!(
-                            output,
-                            "User edited {:?}:\n`````diff\n{}\n`````\n",
-                            path, diff
-                        )
-                        .unwrap();
-                    }
-                }
-            }
+        writeln!(output, "`````diff").unwrap();
+        for event in events {
+            writeln!(output, "{}", event).unwrap();
         }
+        writeln!(output, "`````\n").unwrap();
     }
 
     fn push_file_snippets(