Merge remote-tracking branch 'origin/main' into agent-drawer

Eric Holk created

Change summary

.github/workflows/extension_bump.yml                |  61 +++
crates/agent_servers/src/acp.rs                     |   2 
crates/agent_servers/src/custom.rs                  |  33 -
crates/agent_ui/src/agent_panel.rs                  | 253 +-------------
crates/agent_ui/src/agent_ui.rs                     |  87 -----
tooling/xtask/src/tasks/workflows/extension_bump.rs |  85 ++++
6 files changed, 157 insertions(+), 364 deletions(-)

Detailed changes

.github/workflows/extension_bump.yml 🔗

@@ -105,7 +105,7 @@ jobs:
             --no-configured-files "$BUMP_TYPE" "${BUMP_FILES[@]}"
 
         if [[ -f "Cargo.toml" ]]; then
-            cargo update --workspace
+            cargo +stable update --workspace
         fi
 
         NEW_VERSION="$(sed -n 's/^version = \"\(.*\)\"/\1/p' < extension.toml | tr -d '[:space:]')"
@@ -229,14 +229,69 @@ jobs:
         EXTENSION_ID="$(sed -n 's/id = \"\(.*\)\"/\1/p' < extension.toml)"
 
         echo "extension_id=${EXTENSION_ID}" >> "$GITHUB_OUTPUT"
-    - name: extension_bump::release_action
-      uses: zed-extensions/update-action@1ef53b23be40fe2549be0baffaa98e9f51838fef
+    - id: extension-update
+      name: extension_bump::release_action
+      uses: zed-extensions/update-action@72da482880c2f32ec8aa6e0a0427ab92d52ae32d
       with:
         extension-name: ${{ steps.get-extension-id.outputs.extension_id }}
         push-to: zed-industries/extensions
         tag: ${{ needs.create_version_label.outputs.tag }}
       env:
         COMMITTER_TOKEN: ${{ steps.generate-token.outputs.token }}
+    - name: extension_bump::enable_automerge_if_staff
+      uses: actions/github-script@v7
+      with:
+        github-token: ${{ steps.generate-token.outputs.token }}
+        script: |
+          const prNumber = process.env.PR_NUMBER;
+          if (!prNumber) {
+              console.log('No pull request number set, skipping automerge.');
+              return;
+          }
+
+          const author = process.env.GITHUB_ACTOR;
+          let isStaff = false;
+          try {
+              const response = await github.rest.teams.getMembershipForUserInOrg({
+                  org: 'zed-industries',
+                  team_slug: 'staff',
+                  username: author
+              });
+              isStaff = response.data.state === 'active';
+          } catch (error) {
+              if (error.status !== 404) {
+                  throw error;
+              }
+          }
+
+          if (!isStaff) {
+              console.log(`Actor ${author} is not a staff member, skipping automerge.`);
+              return;
+          }
+
+
+          // Get the GraphQL node ID
+          const { data: pr } = await github.rest.pulls.get({
+              owner: 'zed-industries',
+              repo: 'extensions',
+              pull_number: parseInt(prNumber)
+          });
+
+          await github.graphql(`
+              mutation($pullRequestId: ID!) {
+                  enablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId, mergeMethod: SQUASH }) {
+                      pullRequest {
+                          autoMergeRequest {
+                              enabledAt
+                          }
+                      }
+                  }
+              }
+          `, { pullRequestId: pr.node_id });
+
+          console.log(`Automerge enabled for PR #${prNumber} in zed-industries/extensions`);
+      env:
+        PR_NUMBER: ${{ steps.extension-update.outputs.pull-request-number }}
     defaults:
       run:
         shell: bash -euxo pipefail {0}

crates/agent_servers/src/acp.rs 🔗

@@ -324,7 +324,7 @@ impl AcpConnection {
         // TODO: Remove this override once Google team releases their official auth methods
         let auth_methods = if agent_id.0.as_ref() == GEMINI_ID {
             let mut args = command.args.clone();
-            args.retain(|a| a != "--experimental-acp");
+            args.retain(|a| a != "--experimental-acp" && a != "--acp");
             let value = serde_json::json!({
                 "label": "gemini /auth",
                 "command": command.path.to_string_lossy().into_owned(),

crates/agent_servers/src/custom.rs 🔗

@@ -405,8 +405,6 @@ fn api_key_for_gemini_cli(cx: &mut App) -> Task<Result<String>> {
 
 fn is_registry_agent(agent_id: impl Into<AgentId>, cx: &App) -> bool {
     let agent_id = agent_id.into();
-    let is_previous_built_in =
-        matches!(agent_id.0.as_ref(), CLAUDE_AGENT_ID | CODEX_ID | GEMINI_ID);
     let is_in_registry = project::AgentRegistryStore::try_global(cx)
         .map(|store| store.read(cx).agent(&agent_id).is_some())
         .unwrap_or(false);
@@ -421,7 +419,7 @@ fn is_registry_agent(agent_id: impl Into<AgentId>, cx: &App) -> bool {
                 )
             })
     });
-    is_previous_built_in || is_in_registry || is_settings_registry
+    is_in_registry || is_settings_registry
 }
 
 fn default_settings_for_agent(
@@ -509,16 +507,6 @@ mod tests {
         });
     }
 
-    #[gpui::test]
-    fn test_previous_builtins_are_registry(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.update(|cx| {
-            assert!(is_registry_agent(CLAUDE_AGENT_ID, cx));
-            assert!(is_registry_agent(CODEX_ID, cx));
-            assert!(is_registry_agent(GEMINI_ID, cx));
-        });
-    }
-
     #[gpui::test]
     fn test_unknown_agent_is_not_registry(cx: &mut TestAppContext) {
         init_test(cx);
@@ -581,25 +569,6 @@ mod tests {
         });
     }
 
-    #[gpui::test]
-    fn test_default_settings_for_builtin_agent(cx: &mut TestAppContext) {
-        init_test(cx);
-        cx.update(|cx| {
-            assert!(matches!(
-                default_settings_for_agent(CODEX_ID, cx),
-                settings::CustomAgentServerSettings::Registry { .. }
-            ));
-            assert!(matches!(
-                default_settings_for_agent(CLAUDE_AGENT_ID, cx),
-                settings::CustomAgentServerSettings::Registry { .. }
-            ));
-            assert!(matches!(
-                default_settings_for_agent(GEMINI_ID, cx),
-                settings::CustomAgentServerSettings::Registry { .. }
-            ));
-        });
-    }
-
     #[gpui::test]
     fn test_default_settings_for_extension_agent(cx: &mut TestAppContext) {
         init_test(cx);

crates/agent_ui/src/agent_panel.rs 🔗

@@ -16,10 +16,7 @@ use agent_servers::AgentServer;
 use collections::HashSet;
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use itertools::Itertools;
-use project::{
-    AgentId,
-    agent_server_store::{CLAUDE_AGENT_ID, CODEX_ID, GEMINI_ID},
-};
+use project::AgentId;
 use serde::{Deserialize, Serialize};
 use settings::{LanguageModelProviderSetting, LanguageModelSelection};
 
@@ -583,7 +580,7 @@ enum WhichFontSize {
 }
 
 // TODO unify this with ExternalAgent
-#[derive(Debug, Default, Clone, PartialEq, Serialize)]
+#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
 pub enum AgentType {
     #[default]
     NativeAgent,
@@ -594,65 +591,6 @@ pub enum AgentType {
     },
 }
 
-// Custom impl handles legacy variant names from before the built-in agents were moved to
-// the registry: "ClaudeAgent" -> Custom { name: "claude-acp" }, "Codex" -> Custom { name:
-// "codex-acp" }, "Gemini" -> Custom { name: "gemini" }.
-// Can be removed at some point in the future and go back to #[derive(Deserialize)].
-impl<'de> Deserialize<'de> for AgentType {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        let value = serde_json::Value::deserialize(deserializer)?;
-
-        if let Some(s) = value.as_str() {
-            return match s {
-                "NativeAgent" => Ok(Self::NativeAgent),
-                "TextThread" => Ok(Self::TextThread),
-                "ClaudeAgent" | "ClaudeCode" => Ok(Self::Custom {
-                    id: CLAUDE_AGENT_ID.into(),
-                }),
-                "Codex" => Ok(Self::Custom {
-                    id: CODEX_ID.into(),
-                }),
-                "Gemini" => Ok(Self::Custom {
-                    id: GEMINI_ID.into(),
-                }),
-                other => Err(serde::de::Error::unknown_variant(
-                    other,
-                    &[
-                        "NativeAgent",
-                        "TextThread",
-                        "Custom",
-                        "ClaudeAgent",
-                        "ClaudeCode",
-                        "Codex",
-                        "Gemini",
-                    ],
-                )),
-            };
-        }
-
-        if let Some(obj) = value.as_object() {
-            if let Some(inner) = obj.get("Custom") {
-                #[derive(Deserialize)]
-                struct CustomFields {
-                    name: SharedString,
-                }
-                let fields: CustomFields =
-                    serde_json::from_value(inner.clone()).map_err(serde::de::Error::custom)?;
-                return Ok(Self::Custom {
-                    id: AgentId::new(fields.name),
-                });
-            }
-        }
-
-        Err(serde::de::Error::custom(
-            "expected a string variant or {\"Custom\": {\"name\": ...}}",
-        ))
-    }
-}
-
 impl AgentType {
     pub fn is_native(&self) -> bool {
         matches!(self, Self::NativeAgent)
@@ -3756,10 +3694,8 @@ impl AgentPanel {
                         .header("External Agents")
                         .map(|mut menu| {
                             let agent_server_store = agent_server_store.read(cx);
-                            let registry_store =
-                                project::AgentRegistryStore::try_global(cx);
-                            let registry_store_ref =
-                                registry_store.as_ref().map(|s| s.read(cx));
+                            let registry_store = project::AgentRegistryStore::try_global(cx);
+                            let registry_store_ref = registry_store.as_ref().map(|s| s.read(cx));
 
                             struct AgentMenuItem {
                                 id: AgentId,
@@ -3787,12 +3723,10 @@ impl AgentPanel {
                                 .collect::<Vec<_>>();
 
                             for item in &agent_items {
-                                let mut entry =
-                                    ContextMenuEntry::new(item.display_name.clone());
+                                let mut entry = ContextMenuEntry::new(item.display_name.clone());
 
-                                let icon_path = agent_server_store
-                                    .agent_icon(&item.id)
-                                    .or_else(|| {
+                                let icon_path =
+                                    agent_server_store.agent_icon(&item.id).or_else(|| {
                                         registry_store_ref
                                             .as_ref()
                                             .and_then(|store| store.agent(&item.id))
@@ -3811,9 +3745,9 @@ impl AgentPanel {
                                             id: item.id.clone(),
                                         }),
                                         |this| {
-                                            this.action(Box::new(
-                                                NewExternalAgentThread { agent: None },
-                                            ))
+                                            this.action(Box::new(NewExternalAgentThread {
+                                                agent: None,
+                                            }))
                                         },
                                     )
                                     .icon_color(Color::Muted)
@@ -3848,111 +3782,14 @@ impl AgentPanel {
                             menu
                         })
                         .separator()
-                        .map(|mut menu| {
-                            let agent_server_store = agent_server_store.read(cx);
-                            let registry_store =
-                                project::AgentRegistryStore::try_global(cx);
-                            let registry_store_ref =
-                                registry_store.as_ref().map(|s| s.read(cx));
-
-                            let previous_built_in_ids: &[AgentId] =
-                                &[CLAUDE_AGENT_ID.into(), CODEX_ID.into(), GEMINI_ID.into()];
-
-                            let promoted_items = previous_built_in_ids
-                                .iter()
-                                .filter(|id| {
-                                    !agent_server_store.external_agents.contains_key(*id)
-                                })
-                                .filter_map(|id| {
-                                    let display_name = registry_store_ref
-                                        .as_ref()
-                                        .and_then(|store| store.agent(&id))
-                                        .map(|a| a.name().clone())?;
-                                    Some((id.clone(), display_name))
-                                })
-                                .sorted_unstable_by_key(|(_, display_name)| display_name.to_lowercase())
-                                .collect::<Vec<_>>();
-
-                            for (agent_id, display_name) in &promoted_items {
-                                let mut entry =
-                                    ContextMenuEntry::new(display_name.clone());
-
-                                let icon_path = registry_store_ref
-                                    .as_ref()
-                                    .and_then(|store| store.agent(agent_id))
-                                    .and_then(|a| a.icon_path().cloned());
-
-                                if let Some(icon_path) = icon_path {
-                                    entry = entry.custom_icon_svg(icon_path);
-                                } else {
-                                    entry = entry.icon(IconName::Sparkle);
-                                }
-
-                                entry = entry
-                                    .icon_color(Color::Muted)
-                                    .disabled(is_via_collab)
-                                    .handler({
-                                        let workspace = workspace.clone();
-                                        let agent_id = agent_id.clone();
-                                        move |window, cx| {
-                                            let fs = <dyn fs::Fs>::global(cx);
-                                            let agent_id_string =
-                                                agent_id.to_string();
-                                            settings::update_settings_file(
-                                                fs,
-                                                cx,
-                                                move |settings, _| {
-                                                    let agent_servers = settings
-                                                        .agent_servers
-                                                        .get_or_insert_default();
-                                                    agent_servers.entry(agent_id_string).or_insert_with(|| {
-                                                        settings::CustomAgentServerSettings::Registry {
-                                                            default_mode: None,
-                                                            default_model: None,
-                                                            env: Default::default(),
-                                                            favorite_models: Vec::new(),
-                                                            default_config_options: Default::default(),
-                                                            favorite_config_option_values: Default::default(),
-                                                        }
-                                                    });
-                                                },
-                                            );
-
-                                            if let Some(workspace) = workspace.upgrade() {
-                                                workspace.update(cx, |workspace, cx| {
-                                                    if let Some(panel) =
-                                                        workspace.drawer::<AgentPanel>()
-                                                    {
-                                                        panel.update(cx, |panel, cx| {
-                                                            panel.new_agent_thread(
-                                                                AgentType::Custom {
-                                                                    id: agent_id.clone(),
-                                                                },
-                                                                window,
-                                                                cx,
-                                                            );
-                                                        });
-                                                    }
-                                                });
-                                            }
-                                        }
-                                    });
-
-                                menu = menu.item(entry);
-                            }
-
-                            menu
-                        })
                         .item(
                             ContextMenuEntry::new("Add More Agents")
                                 .icon(IconName::Plus)
                                 .icon_color(Color::Muted)
                                 .handler({
                                     move |window, cx| {
-                                        window.dispatch_action(
-                                            Box::new(zed_actions::AcpRegistry),
-                                            cx,
-                                        )
+                                        window
+                                            .dispatch_action(Box::new(zed_actions::AcpRegistry), cx)
                                     }
                                 }),
                         )
@@ -5020,6 +4857,7 @@ mod tests {
     use fs::FakeFs;
     use gpui::{TestAppContext, VisualTestContext};
     use project::Project;
+    use project::agent_server_store::CODEX_ID;
     use serde_json::json;
     use workspace::MultiWorkspace;
 
@@ -5945,35 +5783,7 @@ mod tests {
     }
 
     #[test]
-    fn test_deserialize_legacy_agent_type_variants() {
-        assert_eq!(
-            serde_json::from_str::<AgentType>(r#""ClaudeAgent""#).unwrap(),
-            AgentType::Custom {
-                id: CLAUDE_AGENT_ID.into(),
-            },
-        );
-        assert_eq!(
-            serde_json::from_str::<AgentType>(r#""ClaudeCode""#).unwrap(),
-            AgentType::Custom {
-                id: CLAUDE_AGENT_ID.into(),
-            },
-        );
-        assert_eq!(
-            serde_json::from_str::<AgentType>(r#""Codex""#).unwrap(),
-            AgentType::Custom {
-                id: CODEX_ID.into(),
-            },
-        );
-        assert_eq!(
-            serde_json::from_str::<AgentType>(r#""Gemini""#).unwrap(),
-            AgentType::Custom {
-                id: GEMINI_ID.into(),
-            },
-        );
-    }
-
-    #[test]
-    fn test_deserialize_current_agent_type_variants() {
+    fn test_deserialize_agent_type_variants() {
         assert_eq!(
             serde_json::from_str::<AgentType>(r#""NativeAgent""#).unwrap(),
             AgentType::NativeAgent,
@@ -5990,8 +5800,6 @@ mod tests {
         );
     }
 
-    const CODEX_NAME: &str = "codex-acp";
-
     #[gpui::test]
     async fn test_worktree_creation_preserves_selected_agent(cx: &mut TestAppContext) {
         init_test(cx);
@@ -6085,7 +5893,7 @@ mod tests {
         // open_external_thread_with_server overrides selected_agent_type.
         panel.update(cx, |panel, cx| {
             panel.selected_agent_type = AgentType::Custom {
-                id: CODEX_NAME.into(),
+                id: CODEX_ID.into(),
             };
             panel.set_start_thread_in(&StartThreadIn::NewWorktree, cx);
         });
@@ -6095,7 +5903,7 @@ mod tests {
             assert_eq!(
                 panel.selected_agent_type,
                 AgentType::Custom {
-                    id: CODEX_NAME.into()
+                    id: CODEX_ID.into()
                 },
             );
         });
@@ -6140,36 +5948,9 @@ mod tests {
         assert_eq!(
             found_codex,
             AgentType::Custom {
-                id: CODEX_NAME.into()
+                id: CODEX_ID.into()
             },
             "the new worktree workspace should use the same agent (Codex) that was selected in the original panel",
         );
     }
-
-    #[test]
-    fn test_deserialize_legacy_serialized_panel() {
-        let json = serde_json::json!({
-            "width": 300.0,
-            "selected_agent": "ClaudeAgent",
-            "last_active_thread": {
-                "session_id": "test-session",
-                "agent_type": "Codex",
-            },
-        });
-
-        let panel: SerializedAgentPanel = serde_json::from_value(json).unwrap();
-        assert_eq!(
-            panel.selected_agent,
-            Some(AgentType::Custom {
-                id: CLAUDE_AGENT_ID.into(),
-            }),
-        );
-        let thread = panel.last_active_thread.unwrap();
-        assert_eq!(
-            thread.agent_type,
-            AgentType::Custom {
-                id: CODEX_ID.into(),
-            },
-        );
-    }
 }

crates/agent_ui/src/agent_ui.rs 🔗

@@ -219,7 +219,7 @@ pub struct NewNativeAgentThreadFromSummary {
 }
 
 // TODO unify this with AgentType
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, JsonSchema)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum Agent {
     NativeAgent,
@@ -229,65 +229,6 @@ pub enum Agent {
     },
 }
 
-// Custom impl handles legacy variant names from before the built-in agents were moved to
-// the registry: "claude_code" -> Custom { name: "claude-acp" }, "codex" -> Custom { name:
-// "codex-acp" }, "gemini" -> Custom { name: "gemini" }.
-// Can be removed at some point in the future and go back to #[derive(Deserialize)].
-impl<'de> serde::Deserialize<'de> for Agent {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        use project::agent_server_store::{CLAUDE_AGENT_ID, CODEX_ID, GEMINI_ID};
-
-        let value = serde_json::Value::deserialize(deserializer)?;
-
-        if let Some(s) = value.as_str() {
-            return match s {
-                "native_agent" => Ok(Self::NativeAgent),
-                "claude_code" | "claude_agent" => Ok(Self::Custom {
-                    id: CLAUDE_AGENT_ID.into(),
-                }),
-                "codex" => Ok(Self::Custom {
-                    id: CODEX_ID.into(),
-                }),
-                "gemini" => Ok(Self::Custom {
-                    id: GEMINI_ID.into(),
-                }),
-                other => Err(serde::de::Error::unknown_variant(
-                    other,
-                    &[
-                        "native_agent",
-                        "custom",
-                        "claude_agent",
-                        "claude_code",
-                        "codex",
-                        "gemini",
-                    ],
-                )),
-            };
-        }
-
-        if let Some(obj) = value.as_object() {
-            if let Some(inner) = obj.get("custom") {
-                #[derive(serde::Deserialize)]
-                struct CustomFields {
-                    name: SharedString,
-                }
-                let fields: CustomFields =
-                    serde_json::from_value(inner.clone()).map_err(serde::de::Error::custom)?;
-                return Ok(Self::Custom {
-                    id: AgentId::new(fields.name),
-                });
-            }
-        }
-
-        Err(serde::de::Error::custom(
-            "expected a string variant or {\"custom\": {\"name\": ...}}",
-        ))
-    }
-}
-
 impl Agent {
     pub fn server(
         &self,
@@ -760,31 +701,7 @@ mod tests {
     }
 
     #[test]
-    fn test_deserialize_legacy_external_agent_variants() {
-        use project::agent_server_store::{CLAUDE_AGENT_ID, CODEX_ID, GEMINI_ID};
-
-        assert_eq!(
-            serde_json::from_str::<Agent>(r#""claude_code""#).unwrap(),
-            Agent::Custom {
-                id: CLAUDE_AGENT_ID.into(),
-            },
-        );
-        assert_eq!(
-            serde_json::from_str::<Agent>(r#""codex""#).unwrap(),
-            Agent::Custom {
-                id: CODEX_ID.into(),
-            },
-        );
-        assert_eq!(
-            serde_json::from_str::<Agent>(r#""gemini""#).unwrap(),
-            Agent::Custom {
-                id: GEMINI_ID.into(),
-            },
-        );
-    }
-
-    #[test]
-    fn test_deserialize_current_external_agent_variants() {
+    fn test_deserialize_external_agent_variants() {
         assert_eq!(
             serde_json::from_str::<Agent>(r#""native_agent""#).unwrap(),
             Agent::NativeAgent,

tooling/xtask/src/tasks/workflows/extension_bump.rs 🔗

@@ -316,7 +316,7 @@ fn bump_version(
             --no-configured-files "$BUMP_TYPE" "${{BUMP_FILES[@]}}"
 
         if [[ -f "Cargo.toml" ]]; then
-            cargo update --workspace
+            cargo +stable update --workspace
         fi
 
         NEW_VERSION="$({VERSION_CHECK})"
@@ -395,6 +395,7 @@ fn trigger_release(
         Some(extension_registry),
     );
     let (get_extension_id, extension_id) = get_extension_id();
+    let (release_action, pull_request_number) = release_action(extension_id, tag, &generated_token);
 
     let job = dependant_job(dependencies)
         .defaults(extension_job_defaults())
@@ -403,7 +404,11 @@ fn trigger_release(
         .add_step(generate_token)
         .add_step(checkout_repo())
         .add_step(get_extension_id)
-        .add_step(release_action(extension_id, tag, generated_token));
+        .add_step(release_action)
+        .add_step(enable_automerge_if_staff(
+            pull_request_number,
+            generated_token,
+        ));
 
     named::job(job)
 }
@@ -425,17 +430,83 @@ fn get_extension_id() -> (Step<Run>, StepOutput) {
 fn release_action(
     extension_id: StepOutput,
     tag: JobOutput,
-    generated_token: StepOutput,
-) -> Step<Use> {
-    named::uses(
+    generated_token: &StepOutput,
+) -> (Step<Use>, StepOutput) {
+    let step = named::uses(
         "zed-extensions",
         "update-action",
-        "1ef53b23be40fe2549be0baffaa98e9f51838fef",
+        "72da482880c2f32ec8aa6e0a0427ab92d52ae32d",
     )
+    .id("extension-update")
     .add_with(("extension-name", extension_id.to_string()))
     .add_with(("push-to", "zed-industries/extensions"))
     .add_with(("tag", tag.to_string()))
-    .add_env(("COMMITTER_TOKEN", generated_token.to_string()))
+    .add_env(("COMMITTER_TOKEN", generated_token.to_string()));
+
+    let pull_request_number = StepOutput::new(&step, "pull-request-number");
+
+    (step, pull_request_number)
+}
+
+fn enable_automerge_if_staff(
+    pull_request_number: StepOutput,
+    generated_token: StepOutput,
+) -> Step<Use> {
+    named::uses("actions", "github-script", "v7")
+        .add_with(("github-token", generated_token.to_string()))
+        .add_with((
+            "script",
+            indoc! {r#"
+                const prNumber = process.env.PR_NUMBER;
+                if (!prNumber) {
+                    console.log('No pull request number set, skipping automerge.');
+                    return;
+                }
+
+                const author = process.env.GITHUB_ACTOR;
+                let isStaff = false;
+                try {
+                    const response = await github.rest.teams.getMembershipForUserInOrg({
+                        org: 'zed-industries',
+                        team_slug: 'staff',
+                        username: author
+                    });
+                    isStaff = response.data.state === 'active';
+                } catch (error) {
+                    if (error.status !== 404) {
+                        throw error;
+                    }
+                }
+
+                if (!isStaff) {
+                    console.log(`Actor ${author} is not a staff member, skipping automerge.`);
+                    return;
+                }
+
+
+                // Get the GraphQL node ID
+                const { data: pr } = await github.rest.pulls.get({
+                    owner: 'zed-industries',
+                    repo: 'extensions',
+                    pull_number: parseInt(prNumber)
+                });
+
+                await github.graphql(`
+                    mutation($pullRequestId: ID!) {
+                        enablePullRequestAutoMerge(input: { pullRequestId: $pullRequestId, mergeMethod: SQUASH }) {
+                            pullRequest {
+                                autoMergeRequest {
+                                    enabledAt
+                                }
+                            }
+                        }
+                    }
+                `, { pullRequestId: pr.node_id });
+
+                console.log(`Automerge enabled for PR #${prNumber} in zed-industries/extensions`);
+            "#},
+        ))
+        .add_env(("PR_NUMBER", pull_request_number.to_string()))
 }
 
 fn extension_workflow_secrets() -> (WorkflowSecret, WorkflowSecret) {