settings: Migration for fixing duplicated `agent` keys (#30237)

Oleksiy Syvokon created

As a byproduct, this fixes bug where it's impossible to change Agent
profile

Closes #30000 

Release Notes:

- N/A

Change summary

crates/migrator/src/migrations.rs                       |  6 +
crates/migrator/src/migrations/m_2025_05_08/settings.rs | 26 ++++++
crates/migrator/src/migrator.rs                         | 42 +++++++++++
crates/migrator/src/patterns.rs                         |  4 
crates/migrator/src/patterns/settings.rs                | 15 +++
5 files changed, 91 insertions(+), 2 deletions(-)

Detailed changes

crates/migrator/src/migrations.rs πŸ”—

@@ -63,3 +63,9 @@ pub(crate) mod m_2025_05_05 {
 
     pub(crate) use settings::SETTINGS_PATTERNS;
 }
+
+pub(crate) mod m_2025_05_08 {
+    mod settings;
+
+    pub(crate) use settings::SETTINGS_PATTERNS;
+}

crates/migrator/src/migrations/m_2025_05_08/settings.rs πŸ”—

@@ -0,0 +1,26 @@
+use std::ops::Range;
+use tree_sitter::{Query, QueryMatch};
+
+use crate::{MigrationPatterns, patterns::SETTINGS_DUPLICATED_AGENT_PATTERN};
+
+pub const SETTINGS_PATTERNS: MigrationPatterns =
+    &[(SETTINGS_DUPLICATED_AGENT_PATTERN, comment_duplicated_agent)];
+
+fn comment_duplicated_agent(
+    contents: &str,
+    mat: &QueryMatch,
+    query: &Query,
+) -> Option<(Range<usize>, String)> {
+    let pair_ix = query.capture_index_for_name("pair1")?;
+    let mut range = mat.nodes_for_capture_index(pair_ix).next()?.byte_range();
+
+    // Include the comma into the commented region
+    let rtext = &contents[range.end..];
+    if let Some(comma_index) = rtext.find(',') {
+        range.end += comma_index + 1;
+    }
+
+    let value = contents[range.clone()].to_string();
+    let commented_value = format!("/* Duplicated key auto-commented: {value} */");
+    Some((range, commented_value))
+}

crates/migrator/src/migrator.rs πŸ”—

@@ -140,6 +140,10 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
             migrations::m_2025_05_05::SETTINGS_PATTERNS,
             &SETTINGS_QUERY_2025_05_05,
         ),
+        (
+            migrations::m_2025_05_08::SETTINGS_PATTERNS,
+            &SETTINGS_QUERY_2025_05_08,
+        ),
     ];
     run_migrations(text, migrations)
 }
@@ -230,6 +234,10 @@ define_query!(
     SETTINGS_QUERY_2025_05_05,
     migrations::m_2025_05_05::SETTINGS_PATTERNS
 );
+define_query!(
+    SETTINGS_QUERY_2025_05_08,
+    migrations::m_2025_05_08::SETTINGS_PATTERNS
+);
 
 // custom query
 static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
@@ -743,4 +751,38 @@ mod tests {
             ),
         );
     }
+
+    #[test]
+    fn test_comment_duplicated_agent() {
+        assert_migrate_settings(
+            r#"{
+                "agent": {
+                    "name": "assistant-1",
+                "model": "gpt-4", // weird formatting
+                    "utf8": "ΠΏΡ€ΠΈΠ²Ρ–Ρ‚"
+                },
+                "something": "else",
+                "agent": {
+                    "name": "assistant-2",
+                    "model": "gemini-pro"
+                }
+            }
+        "#,
+            Some(
+                r#"{
+                /* Duplicated key auto-commented: "agent": {
+                    "name": "assistant-1",
+                "model": "gpt-4", // weird formatting
+                    "utf8": "ΠΏΡ€ΠΈΠ²Ρ–Ρ‚"
+                }, */
+                "something": "else",
+                "agent": {
+                    "name": "assistant-2",
+                    "model": "gemini-pro"
+                }
+            }
+        "#,
+            ),
+        );
+    }
 }

crates/migrator/src/patterns.rs πŸ”—

@@ -8,6 +8,6 @@ pub(crate) use keymap::{
 
 pub(crate) use settings::{
     SETTINGS_ASSISTANT_PATTERN, SETTINGS_ASSISTANT_TOOLS_PATTERN,
-    SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN, SETTINGS_LANGUAGES_PATTERN,
-    SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
+    SETTINGS_DUPLICATED_AGENT_PATTERN, SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN,
+    SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
 };

crates/migrator/src/patterns/settings.rs πŸ”—

@@ -93,3 +93,18 @@ pub const SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN: &str = r#"(document
     (#eq? @edit_predictions "edit_predictions")
     (#eq? @enabled_in_assistant "enabled_in_assistant")
 )"#;
+
+pub const SETTINGS_DUPLICATED_AGENT_PATTERN: &str = r#"(document
+    (object
+        (pair
+            key: (string (string_content) @agent1)
+            value: (_)
+        ) @pair1
+        (pair
+            key: (string (string_content) @agent2)
+            value: (_)
+        )
+    )
+    (#eq? @agent1 "agent")
+    (#eq? @agent2 "agent")
+)"#;