Merge branch 'main' into reuse-nightly

Conrad Irwin created

Change summary

.github/workflows/eval.yml                           |  71 --
.github/workflows/run_agent_evals.yml                |  62 ++
.github/workflows/run_unit_evals.yml                 |  63 ++
.github/workflows/unit_evals.yml                     |  86 ---
Cargo.lock                                           |   3 
crates/acp_tools/src/acp_tools.rs                    |  51 +
crates/agent_ui/src/acp/thread_view.rs               | 160 +++++
crates/agent_ui/src/agent_configuration.rs           |  87 +++
crates/agent_ui/src/agent_diff.rs                    |  26 
crates/agent_ui/src/agent_panel.rs                   |  19 
crates/auto_update/Cargo.toml                        |   1 
crates/auto_update/src/auto_update.rs                |   2 
crates/cloud_zeta2_prompt/src/cloud_zeta2_prompt.rs  |   2 
crates/dap_adapters/src/dap_adapters.rs              |  86 +-
crates/dap_adapters/src/python.rs                    |  75 ++
crates/extensions_ui/src/extensions_ui.rs            |  25 
crates/fs/src/fs.rs                                  |   2 
crates/title_bar/src/collab.rs                       |   2 
crates/ui/src/components/popover_menu.rs             |   2 
crates/zeta2/src/related_excerpts.rs                 |   2 
crates/zeta_cli/Cargo.toml                           |   2 
crates/zeta_cli/src/example.rs                       | 355 ++++++++++++++
crates/zeta_cli/src/main.rs                          |  17 
docs/src/SUMMARY.md                                  |   1 
docs/src/development.md                              |   1 
docs/src/development/releases.md                     | 147 -----
docs/src/extensions/icon-themes.md                   |   2 
docs/src/extensions/languages.md                     |   2 
docs/src/icon-themes.md                              |  10 
docs/src/languages/rego.md                           |   2 
docs/src/snippets.md                                 |   2 
docs/src/themes.md                                   |  23 
docs/src/visual-customization.md                     |  24 
script/run-unit-evals                                |   5 
tooling/xtask/src/tasks/workflows.rs                 |   3 
tooling/xtask/src/tasks/workflows/run_agent_evals.rs | 113 ++++
36 files changed, 1,059 insertions(+), 477 deletions(-)

Detailed changes

.github/workflows/eval.yml πŸ”—

@@ -1,71 +0,0 @@
-name: Run Agent Eval
-
-on:
-  schedule:
-    - cron: "0 0 * * *"
-
-  pull_request:
-    branches:
-      - "**"
-    types: [synchronize, reopened, labeled]
-
-  workflow_dispatch:
-
-concurrency:
-  # Allow only one workflow per any non-`main` branch.
-  group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
-  cancel-in-progress: true
-
-env:
-  CARGO_TERM_COLOR: always
-  CARGO_INCREMENTAL: 0
-  RUST_BACKTRACE: 1
-  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
-  ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
-  ZED_EVAL_TELEMETRY: 1
-
-jobs:
-  run_eval:
-    timeout-minutes: 60
-    name: Run Agent Eval
-    if: >
-      github.repository_owner == 'zed-industries' &&
-      (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
-    runs-on:
-      - namespace-profile-16x32-ubuntu-2204
-    steps:
-      - name: Add Rust to the PATH
-        run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
-
-      - name: Checkout repo
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-        with:
-          clean: false
-
-      - name: Cache dependencies
-        uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
-        with:
-          save-if: ${{ github.ref == 'refs/heads/main' }}
-          # cache-provider: "buildjet"
-
-      - name: Install Linux dependencies
-        run: ./script/linux
-
-      - name: Configure CI
-        run: |
-          mkdir -p ./../.cargo
-          cp ./.cargo/ci-config.toml ./../.cargo/config.toml
-
-      - name: Compile eval
-        run: cargo build --package=eval
-
-      - name: Run eval
-        run: cargo run --package=eval -- --repetitions=8 --concurrency=1
-
-      # Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
-      # But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
-      # to clean up the config file, I’ve included the cleanup code here as a precaution.
-      # While it’s not strictly necessary at this moment, I believe it’s better to err on the side of caution.
-      - name: Clean CI config file
-        if: always()
-        run: rm -rf ./../.cargo

.github/workflows/run_agent_evals.yml πŸ”—

@@ -0,0 +1,62 @@
+# Generated from xtask::workflows::run_agent_evals
+# Rebuild with `cargo xtask workflows`.
+name: run_agent_evals
+env:
+  CARGO_TERM_COLOR: always
+  CARGO_INCREMENTAL: '0'
+  RUST_BACKTRACE: '1'
+  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+  ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
+  ZED_EVAL_TELEMETRY: '1'
+on:
+  pull_request:
+    types:
+    - synchronize
+    - reopened
+    - labeled
+    branches:
+    - '**'
+  schedule:
+  - cron: 0 0 * * *
+  workflow_dispatch: {}
+jobs:
+  agent_evals:
+    if: |
+      github.repository_owner == 'zed-industries' &&
+      (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
+    runs-on: namespace-profile-16x32-ubuntu-2204
+    steps:
+    - name: steps::checkout_repo
+      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      with:
+        clean: false
+    - name: steps::cache_rust_dependencies
+      uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6
+      with:
+        save-if: ${{ github.ref == 'refs/heads/main' }}
+    - name: steps::setup_linux
+      run: ./script/linux
+      shell: bash -euxo pipefail {0}
+    - name: steps::install_mold
+      run: ./script/install-mold
+      shell: bash -euxo pipefail {0}
+    - name: steps::setup_cargo_config
+      run: |
+        mkdir -p ./../.cargo
+        cp ./.cargo/ci-config.toml ./../.cargo/config.toml
+      shell: bash -euxo pipefail {0}
+    - name: cargo build --package=eval
+      run: cargo build --package=eval
+      shell: bash -euxo pipefail {0}
+    - name: run_agent_evals::agent_evals::run_eval
+      run: cargo run --package=eval -- --repetitions=8 --concurrency=1
+      shell: bash -euxo pipefail {0}
+    - name: steps::cleanup_cargo_config
+      if: always()
+      run: |
+        rm -rf ./../.cargo
+      shell: bash -euxo pipefail {0}
+    timeout-minutes: 60
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
+  cancel-in-progress: true

.github/workflows/run_unit_evals.yml πŸ”—

@@ -0,0 +1,63 @@
+# Generated from xtask::workflows::run_agent_evals
+# Rebuild with `cargo xtask workflows`.
+name: run_agent_evals
+env:
+  CARGO_TERM_COLOR: always
+  CARGO_INCREMENTAL: '0'
+  RUST_BACKTRACE: '1'
+  ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
+on:
+  schedule:
+  - cron: 47 1 * * 2
+  workflow_dispatch: {}
+jobs:
+  unit_evals:
+    runs-on: namespace-profile-16x32-ubuntu-2204
+    steps:
+    - name: steps::checkout_repo
+      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      with:
+        clean: false
+    - name: steps::setup_cargo_config
+      run: |
+        mkdir -p ./../.cargo
+        cp ./.cargo/ci-config.toml ./../.cargo/config.toml
+      shell: bash -euxo pipefail {0}
+    - name: steps::cache_rust_dependencies
+      uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6
+      with:
+        save-if: ${{ github.ref == 'refs/heads/main' }}
+    - name: steps::setup_linux
+      run: ./script/linux
+      shell: bash -euxo pipefail {0}
+    - name: steps::install_mold
+      run: ./script/install-mold
+      shell: bash -euxo pipefail {0}
+    - name: steps::cargo_install_nextest
+      run: cargo install cargo-nextest --locked
+      shell: bash -euxo pipefail {0}
+    - name: steps::clear_target_dir_if_large
+      run: ./script/clear-target-dir-if-larger-than 100
+      shell: bash -euxo pipefail {0}
+    - name: ./script/run-unit-evals
+      run: ./script/run-unit-evals
+      shell: bash -euxo pipefail {0}
+      env:
+        ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+    - name: run_agent_evals::unit_evals::send_failure_to_slack
+      if: ${{ failure() }}
+      uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
+      with:
+        method: chat.postMessage
+        token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}
+        payload: |
+          channel: C04UDRNNJFQ
+          text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
+    - name: steps::cleanup_cargo_config
+      if: always()
+      run: |
+        rm -rf ./../.cargo
+      shell: bash -euxo pipefail {0}
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
+  cancel-in-progress: true

.github/workflows/unit_evals.yml πŸ”—

@@ -1,86 +0,0 @@
-name: Run Unit Evals
-
-on:
-  schedule:
-    # GitHub might drop jobs at busy times, so we choose a random time in the middle of the night.
-    - cron: "47 1 * * 2"
-  workflow_dispatch:
-
-concurrency:
-  # Allow only one workflow per any non-`main` branch.
-  group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
-  cancel-in-progress: true
-
-env:
-  CARGO_TERM_COLOR: always
-  CARGO_INCREMENTAL: 0
-  RUST_BACKTRACE: 1
-  ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
-
-jobs:
-  unit_evals:
-    if: github.repository_owner == 'zed-industries'
-    timeout-minutes: 60
-    name: Run unit evals
-    runs-on:
-      - namespace-profile-16x32-ubuntu-2204
-    steps:
-      - name: Add Rust to the PATH
-        run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
-
-      - name: Checkout repo
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-        with:
-          clean: false
-
-      - name: Cache dependencies
-        uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
-        with:
-          save-if: ${{ github.ref == 'refs/heads/main' }}
-          # cache-provider: "buildjet"
-
-      - name: Install Linux dependencies
-        run: ./script/linux
-
-      - name: Configure CI
-        run: |
-          mkdir -p ./../.cargo
-          cp ./.cargo/ci-config.toml ./../.cargo/config.toml
-
-      - name: Install Rust
-        shell: bash -euxo pipefail {0}
-        run: |
-          cargo install cargo-nextest --locked
-
-      - name: Install Node
-        uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
-        with:
-          node-version: "18"
-
-      - name: Limit target directory size
-        shell: bash -euxo pipefail {0}
-        run: script/clear-target-dir-if-larger-than 100
-
-      - name: Run unit evals
-        shell: bash -euxo pipefail {0}
-        run: cargo nextest run --workspace --no-fail-fast --features unit-eval --no-capture -E 'test(::eval_)'
-        env:
-          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
-
-      - name: Send failure message to Slack channel if needed
-        if: ${{ failure() }}
-        uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
-        with:
-          method: chat.postMessage
-          token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}
-          payload: |
-            channel: C04UDRNNJFQ
-            text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
-
-      # Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
-      # But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
-      # to clean up the config file, I’ve included the cleanup code here as a precaution.
-      # While it’s not strictly necessary at this moment, I believe it’s better to err on the side of caution.
-      - name: Clean CI config file
-        if: always()
-        run: rm -rf ./../.cargo

Cargo.lock πŸ”—

@@ -1339,6 +1339,7 @@ dependencies = [
  "settings",
  "smol",
  "tempfile",
+ "util",
  "which 6.0.3",
  "workspace",
 ]
@@ -21757,6 +21758,7 @@ dependencies = [
  "polars",
  "project",
  "prompt_store",
+ "pulldown-cmark 0.12.2",
  "release_channel",
  "reqwest_client",
  "serde",
@@ -21766,6 +21768,7 @@ dependencies = [
  "smol",
  "soa-rs",
  "terminal_view",
+ "toml 0.8.23",
  "util",
  "watch",
  "zeta",

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

@@ -19,7 +19,7 @@ use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
 use project::Project;
 use settings::Settings;
 use theme::ThemeSettings;
-use ui::{Tooltip, prelude::*};
+use ui::{Tooltip, WithScrollbar, prelude::*};
 use util::ResultExt as _;
 use workspace::{
     Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -291,17 +291,19 @@ impl AcpTools {
         let expanded = self.expanded.contains(&index);
 
         v_flex()
+            .id(index)
+            .group("message")
+            .cursor_pointer()
+            .font_buffer(cx)
             .w_full()
-            .px_4()
             .py_3()
-            .border_color(colors.border)
-            .border_b_1()
+            .pl_4()
+            .pr_5()
             .gap_2()
             .items_start()
-            .font_buffer(cx)
             .text_size(base_size)
-            .id(index)
-            .group("message")
+            .border_color(colors.border)
+            .border_b_1()
             .hover(|this| this.bg(colors.element_background.opacity(0.5)))
             .on_click(cx.listener(move |this, _, _, cx| {
                 if this.expanded.contains(&index) {
@@ -323,15 +325,14 @@ impl AcpTools {
                 h_flex()
                     .w_full()
                     .gap_2()
-                    .items_center()
                     .flex_shrink_0()
                     .child(match message.direction {
-                        acp::StreamMessageDirection::Incoming => {
-                            ui::Icon::new(ui::IconName::ArrowDown).color(Color::Error)
-                        }
-                        acp::StreamMessageDirection::Outgoing => {
-                            ui::Icon::new(ui::IconName::ArrowUp).color(Color::Success)
-                        }
+                        acp::StreamMessageDirection::Incoming => Icon::new(IconName::ArrowDown)
+                            .color(Color::Error)
+                            .size(IconSize::Small),
+                        acp::StreamMessageDirection::Outgoing => Icon::new(IconName::ArrowUp)
+                            .color(Color::Success)
+                            .size(IconSize::Small),
                     })
                     .child(
                         Label::new(message.name.clone())
@@ -501,7 +502,7 @@ impl Focusable for AcpTools {
 }
 
 impl Render for AcpTools {
-    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         v_flex()
             .track_focus(&self.focus_handle)
             .size_full()
@@ -516,13 +517,19 @@ impl Render for AcpTools {
                             .child("No messages recorded yet")
                             .into_any()
                     } else {
-                        list(
-                            connection.list_state.clone(),
-                            cx.processor(Self::render_message),
-                        )
-                        .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
-                        .flex_grow()
-                        .into_any()
+                        div()
+                            .size_full()
+                            .flex_grow()
+                            .child(
+                                list(
+                                    connection.list_state.clone(),
+                                    cx.processor(Self::render_message),
+                                )
+                                .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
+                                .size_full(),
+                            )
+                            .vertical_scrollbar_for(connection.list_state.clone(), window, cx)
+                            .into_any()
                     }
                 }
                 None => h_flex()

crates/agent_ui/src/acp/thread_view.rs πŸ”—

@@ -3631,6 +3631,7 @@ impl AcpThreadView {
             .child(
                 h_flex()
                     .id("edits-container")
+                    .cursor_pointer()
                     .gap_1()
                     .child(Disclosure::new("edits-disclosure", expanded))
                     .map(|this| {
@@ -3770,6 +3771,7 @@ impl AcpThreadView {
                     Label::new(name.to_string())
                         .size(LabelSize::XSmall)
                         .buffer_font(cx)
+                        .ml_1p5()
                 });
 
                 let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
@@ -3801,14 +3803,30 @@ impl AcpThreadView {
                     })
                     .child(
                         h_flex()
+                            .id(("file-name-row", index))
                             .relative()
-                            .id(("file-name", index))
                             .pr_8()
-                            .gap_1p5()
                             .w_full()
                             .overflow_x_scroll()
-                            .child(file_icon)
-                            .child(h_flex().gap_0p5().children(file_name).children(file_path))
+                            .child(
+                                h_flex()
+                                    .id(("file-name-path", index))
+                                    .cursor_pointer()
+                                    .pr_0p5()
+                                    .gap_0p5()
+                                    .hover(|s| s.bg(cx.theme().colors().element_hover))
+                                    .rounded_xs()
+                                    .child(file_icon)
+                                    .children(file_name)
+                                    .children(file_path)
+                                    .tooltip(Tooltip::text("Go to File"))
+                                    .on_click({
+                                        let buffer = buffer.clone();
+                                        cx.listener(move |this, _, window, cx| {
+                                            this.open_edited_buffer(&buffer, window, cx);
+                                        })
+                                    }),
+                            )
                             .child(
                                 div()
                                     .absolute()
@@ -3818,13 +3836,7 @@ impl AcpThreadView {
                                     .bottom_0()
                                     .right_0()
                                     .bg(overlay_gradient),
-                            )
-                            .on_click({
-                                let buffer = buffer.clone();
-                                cx.listener(move |this, _, window, cx| {
-                                    this.open_edited_buffer(&buffer, window, cx);
-                                })
-                            }),
+                            ),
                     )
                     .child(
                         h_flex()
@@ -4571,14 +4583,29 @@ impl AcpThreadView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if window.is_window_active() || !self.notifications.is_empty() {
+        if !self.notifications.is_empty() {
+            return;
+        }
+
+        let settings = AgentSettings::get_global(cx);
+
+        let window_is_inactive = !window.is_window_active();
+        let panel_is_hidden = self
+            .workspace
+            .upgrade()
+            .map(|workspace| AgentPanel::is_hidden(&workspace, cx))
+            .unwrap_or(true);
+
+        let should_notify = window_is_inactive || panel_is_hidden;
+
+        if !should_notify {
             return;
         }
 
         // TODO: Change this once we have title summarization for external agents.
         let title = self.agent.name();
 
-        match AgentSettings::get_global(cx).notify_when_agent_waiting {
+        match settings.notify_when_agent_waiting {
             NotifyWhenAgentWaiting::PrimaryScreen => {
                 if let Some(primary) = cx.primary_display() {
                     self.pop_up(icon, caption.into(), title, window, primary, cx);
@@ -5581,7 +5608,7 @@ fn default_markdown_style(
     let theme_settings = ThemeSettings::get_global(cx);
     let colors = cx.theme().colors();
 
-    let buffer_font_size = TextSize::Small.rems(cx);
+    let buffer_font_size = theme_settings.agent_buffer_font_size(cx);
 
     let mut text_style = window.text_style();
     let line_height = buffer_font_size * 1.75;
@@ -5593,9 +5620,9 @@ fn default_markdown_style(
     };
 
     let font_size = if buffer_font {
-        TextSize::Small.rems(cx)
+        theme_settings.agent_buffer_font_size(cx)
     } else {
-        TextSize::Default.rems(cx)
+        theme_settings.agent_ui_font_size(cx)
     };
 
     let text_color = if muted_text {
@@ -5892,6 +5919,107 @@ pub(crate) mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_notification_when_panel_hidden(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
+
+        add_to_workspace(thread_view.clone(), cx);
+
+        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
+
+        message_editor.update_in(cx, |editor, window, cx| {
+            editor.set_text("Hello", window, cx);
+        });
+
+        // Window is active (don't deactivate), but panel will be hidden
+        // Note: In the test environment, the panel is not actually added to the dock,
+        // so is_agent_panel_hidden will return true
+
+        thread_view.update_in(cx, |thread_view, window, cx| {
+            thread_view.send(window, cx);
+        });
+
+        cx.run_until_parked();
+
+        // Should show notification because window is active but panel is hidden
+        assert!(
+            cx.windows()
+                .iter()
+                .any(|window| window.downcast::<AgentNotification>().is_some()),
+            "Expected notification when panel is hidden"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_notification_still_works_when_window_inactive(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
+
+        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
+        message_editor.update_in(cx, |editor, window, cx| {
+            editor.set_text("Hello", window, cx);
+        });
+
+        // Deactivate window - should show notification regardless of setting
+        cx.deactivate_window();
+
+        thread_view.update_in(cx, |thread_view, window, cx| {
+            thread_view.send(window, cx);
+        });
+
+        cx.run_until_parked();
+
+        // Should still show notification when window is inactive (existing behavior)
+        assert!(
+            cx.windows()
+                .iter()
+                .any(|window| window.downcast::<AgentNotification>().is_some()),
+            "Expected notification when window is inactive"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_notification_respects_never_setting(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        // Set notify_when_agent_waiting to Never
+        cx.update(|cx| {
+            AgentSettings::override_global(
+                AgentSettings {
+                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
+                    ..AgentSettings::get_global(cx).clone()
+                },
+                cx,
+            );
+        });
+
+        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
+
+        let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
+        message_editor.update_in(cx, |editor, window, cx| {
+            editor.set_text("Hello", window, cx);
+        });
+
+        // Window is active
+
+        thread_view.update_in(cx, |thread_view, window, cx| {
+            thread_view.send(window, cx);
+        });
+
+        cx.run_until_parked();
+
+        // Should NOT show notification because notify_when_agent_waiting is Never
+        assert!(
+            !cx.windows()
+                .iter()
+                .any(|window| window.downcast::<AgentNotification>().is_some()),
+            "Expected no notification when notify_when_agent_waiting is Never"
+        );
+    }
+
     async fn setup_thread_view(
         agent: impl AgentServer + 'static,
         cx: &mut TestAppContext,

crates/agent_ui/src/agent_configuration.rs πŸ”—

@@ -23,16 +23,18 @@ use language::LanguageRegistry;
 use language_model::{
     LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
 };
+use language_models::AllLanguageModelSettings;
 use notifications::status_toast::{StatusToast, ToastIcon};
 use project::{
     agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
     context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
 };
 use rope::Rope;
-use settings::{SettingsStore, update_settings_file};
+use settings::{Settings, SettingsStore, update_settings_file};
 use ui::{
-    Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
-    Indicator, PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
+    Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor,
+    ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, PopoverMenu, Switch,
+    SwitchColor, Tooltip, WithScrollbar, prelude::*,
 };
 use util::ResultExt as _;
 use workspace::{Workspace, create_and_open_local_file};
@@ -304,10 +306,76 @@ impl AgentConfiguration {
                                 }
                             })),
                         )
-                    }),
+                    })
+                    .when(
+                        is_expanded && is_removable_provider(&provider.id(), cx),
+                        |this| {
+                            this.child(
+                                Button::new(
+                                    SharedString::from(format!("delete-provider-{provider_id}")),
+                                    "Remove Provider",
+                                )
+                                .full_width()
+                                .style(ButtonStyle::Outlined)
+                                .icon_position(IconPosition::Start)
+                                .icon(IconName::Trash)
+                                .icon_size(IconSize::Small)
+                                .icon_color(Color::Muted)
+                                .label_size(LabelSize::Small)
+                                .on_click(cx.listener({
+                                    let provider = provider.clone();
+                                    move |this, _event, window, cx| {
+                                        this.delete_provider(provider.clone(), window, cx);
+                                    }
+                                })),
+                            )
+                        },
+                    ),
             )
     }
 
+    fn delete_provider(
+        &mut self,
+        provider: Arc<dyn LanguageModelProvider>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let fs = self.fs.clone();
+        let provider_id = provider.id();
+
+        cx.spawn_in(window, async move |_, cx| {
+            cx.update(|_window, cx| {
+                update_settings_file(fs.clone(), cx, {
+                    let provider_id = provider_id.clone();
+                    move |settings, _| {
+                        if let Some(ref mut openai_compatible) = settings
+                            .language_models
+                            .as_mut()
+                            .and_then(|lm| lm.openai_compatible.as_mut())
+                        {
+                            let key_to_remove: Arc<str> = Arc::from(provider_id.0.as_ref());
+                            openai_compatible.remove(&key_to_remove);
+                        }
+                    }
+                });
+            })
+            .log_err();
+
+            cx.update(|_window, cx| {
+                LanguageModelRegistry::global(cx).update(cx, {
+                    let provider_id = provider_id.clone();
+                    move |registry, cx| {
+                        registry.unregister_provider(provider_id, cx);
+                    }
+                })
+            })
+            .log_err();
+
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
+
     fn render_provider_configuration_section(
         &mut self,
         cx: &mut Context<Self>,
@@ -1225,3 +1293,14 @@ fn find_text_in_buffer(
         None
     }
 }
+
+// OpenAI-compatible providers are user-configured and can be removed,
+// whereas built-in providers (like Anthropic, OpenAI, Google, etc.) can't.
+//
+// If in the future we have more "API-compatible-type" of providers,
+// they should be included here as removable providers.
+fn is_removable_provider(provider_id: &LanguageModelProviderId, cx: &App) -> bool {
+    AllLanguageModelSettings::get_global(cx)
+        .openai_compatible
+        .contains_key(provider_id.0.as_ref())
+}

crates/agent_ui/src/agent_diff.rs πŸ”—

@@ -70,14 +70,6 @@ impl AgentDiffThread {
         }
     }
 
-    fn is_generating(&self, cx: &App) -> bool {
-        match self {
-            AgentDiffThread::AcpThread(thread) => {
-                thread.read(cx).status() == acp_thread::ThreadStatus::Generating
-            }
-        }
-    }
-
     fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
         match self {
             AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
@@ -970,9 +962,7 @@ impl AgentDiffToolbar {
             None => ToolbarItemLocation::Hidden,
             Some(AgentDiffToolbarItem::Pane(_)) => ToolbarItemLocation::PrimaryRight,
             Some(AgentDiffToolbarItem::Editor { state, .. }) => match state {
-                EditorState::Generating | EditorState::Reviewing => {
-                    ToolbarItemLocation::PrimaryRight
-                }
+                EditorState::Reviewing => ToolbarItemLocation::PrimaryRight,
                 EditorState::Idle => ToolbarItemLocation::Hidden,
             },
         }
@@ -1050,7 +1040,6 @@ impl Render for AgentDiffToolbar {
 
                 let content = match state {
                     EditorState::Idle => return Empty.into_any(),
-                    EditorState::Generating => vec![spinner_icon],
                     EditorState::Reviewing => vec![
                         h_flex()
                             .child(
@@ -1222,7 +1211,6 @@ pub struct AgentDiff {
 pub enum EditorState {
     Idle,
     Reviewing,
-    Generating,
 }
 
 struct WorkspaceThread {
@@ -1545,15 +1533,11 @@ impl AgentDiff {
                     multibuffer.add_diff(diff_handle.clone(), cx);
                 });
 
-                let new_state = if thread.is_generating(cx) {
-                    EditorState::Generating
-                } else {
-                    EditorState::Reviewing
-                };
+                let reviewing_state = EditorState::Reviewing;
 
                 let previous_state = self
                     .reviewing_editors
-                    .insert(weak_editor.clone(), new_state.clone());
+                    .insert(weak_editor.clone(), reviewing_state.clone());
 
                 if previous_state.is_none() {
                     editor.update(cx, |editor, cx| {
@@ -1566,7 +1550,9 @@ impl AgentDiff {
                     unaffected.remove(weak_editor);
                 }
 
-                if new_state == EditorState::Reviewing && previous_state != Some(new_state) {
+                if reviewing_state == EditorState::Reviewing
+                    && previous_state != Some(reviewing_state)
+                {
                     // Jump to first hunk when we enter review mode
                     editor.update(cx, |editor, cx| {
                         let snapshot = multibuffer.read(cx).snapshot(cx);

crates/agent_ui/src/agent_panel.rs πŸ”—

@@ -729,6 +729,25 @@ impl AgentPanel {
         &self.context_server_registry
     }
 
+    pub fn is_hidden(workspace: &Entity<Workspace>, cx: &App) -> bool {
+        let workspace_read = workspace.read(cx);
+
+        workspace_read
+            .panel::<AgentPanel>(cx)
+            .map(|panel| {
+                let panel_id = Entity::entity_id(&panel);
+
+                let is_visible = workspace_read.all_docks().iter().any(|dock| {
+                    dock.read(cx)
+                        .visible_panel()
+                        .is_some_and(|visible_panel| visible_panel.panel_id() == panel_id)
+                });
+
+                !is_visible
+            })
+            .unwrap_or(true)
+    }
+
     fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
         match &self.active_view {
             ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view),

crates/auto_update/Cargo.toml πŸ”—

@@ -26,6 +26,7 @@ serde_json.workspace = true
 settings.workspace = true
 smol.workspace = true
 tempfile.workspace = true
+util.workspace = true
 workspace.workspace = true
 
 [target.'cfg(not(target_os = "windows"))'.dependencies]

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

@@ -962,7 +962,7 @@ pub async fn finalize_auto_update_on_quit() {
             .parent()
             .map(|p| p.join("tools").join("auto_update_helper.exe"))
     {
-        let mut command = smol::process::Command::new(helper);
+        let mut command = util::command::new_smol_command(helper);
         command.arg("--launch");
         command.arg("false");
         if let Ok(mut cmd) = command.spawn() {

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

@@ -212,7 +212,7 @@ pub fn write_codeblock<'a>(
     include_line_numbers: bool,
     output: &'a mut String,
 ) {
-    writeln!(output, "`````path={}", path.display()).unwrap();
+    writeln!(output, "`````{}", path.display()).unwrap();
     write_excerpts(
         excerpts,
         sorted_insertions,

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

@@ -42,61 +42,63 @@ pub fn init(cx: &mut App) {
 }
 
 #[cfg(test)]
-struct MockDelegate {
-    worktree_root: PathBuf,
-}
+mod test_mocks {
+    use super::*;
 
-#[cfg(test)]
-impl MockDelegate {
-    fn new() -> Arc<dyn adapters::DapDelegate> {
-        Arc::new(Self {
-            worktree_root: PathBuf::from("/tmp/test"),
-        })
+    pub(crate) struct MockDelegate {
+        worktree_root: PathBuf,
     }
-}
 
-#[cfg(test)]
-#[async_trait::async_trait]
-impl adapters::DapDelegate for MockDelegate {
-    fn worktree_id(&self) -> settings::WorktreeId {
-        settings::WorktreeId::from_usize(0)
+    impl MockDelegate {
+        pub(crate) fn new() -> Arc<dyn adapters::DapDelegate> {
+            Arc::new(Self {
+                worktree_root: PathBuf::from("/tmp/test"),
+            })
+        }
     }
 
-    fn worktree_root_path(&self) -> &std::path::Path {
-        &self.worktree_root
-    }
+    #[async_trait::async_trait]
+    impl adapters::DapDelegate for MockDelegate {
+        fn worktree_id(&self) -> settings::WorktreeId {
+            settings::WorktreeId::from_usize(0)
+        }
 
-    fn http_client(&self) -> Arc<dyn http_client::HttpClient> {
-        unimplemented!("Not needed for tests")
-    }
+        fn worktree_root_path(&self) -> &std::path::Path {
+            &self.worktree_root
+        }
 
-    fn node_runtime(&self) -> node_runtime::NodeRuntime {
-        unimplemented!("Not needed for tests")
-    }
+        fn http_client(&self) -> Arc<dyn http_client::HttpClient> {
+            unimplemented!("Not needed for tests")
+        }
 
-    fn toolchain_store(&self) -> Arc<dyn language::LanguageToolchainStore> {
-        unimplemented!("Not needed for tests")
-    }
+        fn node_runtime(&self) -> node_runtime::NodeRuntime {
+            unimplemented!("Not needed for tests")
+        }
 
-    fn fs(&self) -> Arc<dyn fs::Fs> {
-        unimplemented!("Not needed for tests")
-    }
+        fn toolchain_store(&self) -> Arc<dyn language::LanguageToolchainStore> {
+            unimplemented!("Not needed for tests")
+        }
 
-    fn output_to_console(&self, _msg: String) {}
+        fn fs(&self) -> Arc<dyn fs::Fs> {
+            unimplemented!("Not needed for tests")
+        }
 
-    async fn which(&self, _command: &std::ffi::OsStr) -> Option<PathBuf> {
-        None
-    }
+        fn output_to_console(&self, _msg: String) {}
 
-    async fn read_text_file(&self, _path: &util::rel_path::RelPath) -> Result<String> {
-        Ok(String::new())
-    }
+        async fn which(&self, _command: &std::ffi::OsStr) -> Option<PathBuf> {
+            None
+        }
 
-    async fn shell_env(&self) -> collections::HashMap<String, String> {
-        collections::HashMap::default()
-    }
+        async fn read_text_file(&self, _path: &util::rel_path::RelPath) -> Result<String> {
+            Ok(String::new())
+        }
 
-    fn is_headless(&self) -> bool {
-        false
+        async fn shell_env(&self) -> collections::HashMap<String, String> {
+            collections::HashMap::default()
+        }
+
+        fn is_headless(&self) -> bool {
+            false
+        }
     }
 }

crates/dap_adapters/src/python.rs πŸ”—

@@ -824,29 +824,58 @@ impl DebugAdapter for PythonDebugAdapter {
                 .await;
         }
 
-        let base_path = config
-            .config
-            .get("cwd")
-            .and_then(|cwd| {
-                RelPath::new(
-                    cwd.as_str()
-                        .map(Path::new)?
-                        .strip_prefix(delegate.worktree_root_path())
-                        .ok()?,
-                    PathStyle::local(),
-                )
-                .ok()
+        let base_paths = ["cwd", "program", "module"]
+            .into_iter()
+            .filter_map(|key| {
+                config.config.get(key).and_then(|cwd| {
+                    RelPath::new(
+                        cwd.as_str()
+                            .map(Path::new)?
+                            .strip_prefix(delegate.worktree_root_path())
+                            .ok()?,
+                        PathStyle::local(),
+                    )
+                    .ok()
+                })
             })
-            .unwrap_or_else(|| RelPath::empty().into());
-        let toolchain = delegate
-            .toolchain_store()
-            .active_toolchain(
-                delegate.worktree_id(),
-                base_path.into_arc(),
-                language::LanguageName::new(Self::LANGUAGE_NAME),
-                cx,
+            .chain(
+                // While Debugpy's wiki saids absolute paths are required, but it actually supports relative paths when cwd is passed in.
+                // (Which should always be the case because Zed defaults to the cwd worktree root)
+                // So we want to check that these relative paths find toolchains as well. Otherwise, they won't be checked
+                // because the strip prefix in the iteration above will return an error
+                config
+                    .config
+                    .get("cwd")
+                    .map(|_| {
+                        ["program", "module"].into_iter().filter_map(|key| {
+                            config.config.get(key).and_then(|value| {
+                                let path = Path::new(value.as_str()?);
+                                RelPath::new(path, PathStyle::local()).ok()
+                            })
+                        })
+                    })
+                    .into_iter()
+                    .flatten(),
             )
-            .await;
+            .chain([RelPath::empty().into()]);
+
+        let mut toolchain = None;
+
+        for base_path in base_paths {
+            if let Some(found_toolchain) = delegate
+                .toolchain_store()
+                .active_toolchain(
+                    delegate.worktree_id(),
+                    base_path.into_arc(),
+                    language::LanguageName::new(Self::LANGUAGE_NAME),
+                    cx,
+                )
+                .await
+            {
+                toolchain = Some(found_toolchain);
+                break;
+            }
+        }
 
         self.fetch_debugpy_whl(toolchain.clone(), delegate)
             .await
@@ -914,7 +943,7 @@ mod tests {
 
         let result = adapter
             .get_installed_binary(
-                &MockDelegate::new(),
+                &test_mocks::MockDelegate::new(),
                 &task_def,
                 None,
                 None,
@@ -955,7 +984,7 @@ mod tests {
 
         let result_host = adapter
             .get_installed_binary(
-                &MockDelegate::new(),
+                &test_mocks::MockDelegate::new(),
                 &task_def_host,
                 None,
                 None,

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

@@ -805,25 +805,22 @@ impl ExtensionsPage {
             )
             .child(
                 h_flex()
-                    .gap_2()
+                    .gap_1()
                     .justify_between()
                     .child(
-                        h_flex()
-                            .gap_1()
-                            .child(
-                                Icon::new(IconName::Person)
-                                    .size(IconSize::XSmall)
-                                    .color(Color::Muted),
-                            )
-                            .child(
-                                Label::new(extension.manifest.authors.join(", "))
-                                    .size(LabelSize::Small)
-                                    .color(Color::Muted)
-                                    .truncate(),
-                            ),
+                        Icon::new(IconName::Person)
+                            .size(IconSize::XSmall)
+                            .color(Color::Muted),
+                    )
+                    .child(
+                        Label::new(extension.manifest.authors.join(", "))
+                            .size(LabelSize::Small)
+                            .color(Color::Muted)
+                            .truncate(),
                     )
                     .child(
                         h_flex()
+                            .ml_auto()
                             .gap_1()
                             .child(
                                 IconButton::new(

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

@@ -377,7 +377,7 @@ impl Fs for RealFs {
 
         #[cfg(windows)]
         if smol::fs::metadata(&target).await?.is_dir() {
-            let status = smol::process::Command::new("cmd")
+            let status = new_smol_command("cmd")
                 .args(["/C", "mklink", "/J"])
                 .args([path, target.as_path()])
                 .status()

crates/title_bar/src/collab.rs πŸ”—

@@ -220,6 +220,8 @@ impl TitleBar {
                                 .on_click({
                                     let peer_id = collaborator.peer_id;
                                     cx.listener(move |this, _, window, cx| {
+                                        cx.stop_propagation();
+
                                         this.workspace
                                             .update(cx, |workspace, cx| {
                                                 if is_following {

crates/ui/src/components/popover_menu.rs πŸ”—

@@ -270,11 +270,11 @@ fn show_menu<M: ManagedView>(
     window: &mut Window,
     cx: &mut App,
 ) {
+    let previous_focus_handle = window.focused(cx);
     let Some(new_menu) = (builder)(window, cx) else {
         return;
     };
     let menu2 = menu.clone();
-    let previous_focus_handle = window.focused(cx);
 
     window
         .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| {

crates/zeta_cli/Cargo.toml πŸ”—

@@ -39,8 +39,10 @@ paths.workspace = true
 polars = { version = "0.51", features = ["lazy", "dtype-struct", "parquet"] }
 project.workspace = true
 prompt_store.workspace = true
+pulldown-cmark.workspace = true
 release_channel.workspace = true
 reqwest_client.workspace = true
+toml.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/zeta_cli/src/example.rs πŸ”—

@@ -0,0 +1,355 @@
+use std::{
+    borrow::Cow,
+    env,
+    fmt::{self, Display},
+    fs,
+    io::Write,
+    mem,
+    path::{Path, PathBuf},
+};
+
+use anyhow::{Context as _, Result};
+use clap::ValueEnum;
+use gpui::http_client::Url;
+use pulldown_cmark::CowStr;
+use serde::{Deserialize, Serialize};
+
+const CURSOR_POSITION_HEADING: &str = "Cursor Position";
+const EDIT_HISTORY_HEADING: &str = "Edit History";
+const EXPECTED_PATCH_HEADING: &str = "Expected Patch";
+const EXPECTED_EXCERPTS_HEADING: &str = "Expected Excerpts";
+const REPOSITORY_URL_FIELD: &str = "repository_url";
+const REVISION_FIELD: &str = "revision";
+
+#[derive(Debug)]
+pub struct NamedExample {
+    pub name: String,
+    pub example: Example,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Example {
+    pub repository_url: String,
+    pub revision: String,
+    pub cursor_path: PathBuf,
+    pub cursor_position: String,
+    pub edit_history: Vec<String>,
+    pub expected_patch: String,
+    pub expected_excerpts: Vec<ExpectedExcerpt>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct ExpectedExcerpt {
+    path: PathBuf,
+    text: String,
+}
+
+#[derive(ValueEnum, Debug, Clone)]
+pub enum ExampleFormat {
+    Json,
+    Toml,
+    Md,
+}
+
+impl NamedExample {
+    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
+        let path = path.as_ref();
+        let content = std::fs::read_to_string(path)?;
+        let ext = path.extension();
+
+        match ext.and_then(|s| s.to_str()) {
+            Some("json") => Ok(Self {
+                name: path.file_name().unwrap_or_default().display().to_string(),
+                example: serde_json::from_str(&content)?,
+            }),
+            Some("toml") => Ok(Self {
+                name: path.file_name().unwrap_or_default().display().to_string(),
+                example: toml::from_str(&content)?,
+            }),
+            Some("md") => Self::parse_md(&content),
+            Some(_) => {
+                anyhow::bail!("Unrecognized example extension: {}", ext.unwrap().display());
+            }
+            None => {
+                anyhow::bail!(
+                    "Failed to determine example type since the file does not have an extension."
+                );
+            }
+        }
+    }
+
+    pub fn parse_md(input: &str) -> Result<Self> {
+        use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Parser, Tag, TagEnd};
+
+        let parser = Parser::new(input);
+
+        let mut named = NamedExample {
+            name: String::new(),
+            example: Example {
+                repository_url: String::new(),
+                revision: String::new(),
+                cursor_path: PathBuf::new(),
+                cursor_position: String::new(),
+                edit_history: Vec::new(),
+                expected_patch: String::new(),
+                expected_excerpts: Vec::new(),
+            },
+        };
+
+        let mut text = String::new();
+        let mut current_section = String::new();
+        let mut block_info: CowStr = "".into();
+
+        for event in parser {
+            match event {
+                Event::Text(line) => {
+                    text.push_str(&line);
+
+                    if !named.name.is_empty()
+                        && current_section.is_empty()
+                        // in h1 section
+                        && let Some((field, value)) = line.split_once('=')
+                    {
+                        match field.trim() {
+                            REPOSITORY_URL_FIELD => {
+                                named.example.repository_url = value.trim().to_string();
+                            }
+                            REVISION_FIELD => {
+                                named.example.revision = value.trim().to_string();
+                            }
+                            _ => {
+                                eprintln!("Warning: Unrecognized field `{field}`");
+                            }
+                        }
+                    }
+                }
+                Event::End(TagEnd::Heading(HeadingLevel::H1)) => {
+                    if !named.name.is_empty() {
+                        anyhow::bail!(
+                            "Found multiple H1 headings. There should only be one with the name of the example."
+                        );
+                    }
+                    named.name = mem::take(&mut text);
+                }
+                Event::End(TagEnd::Heading(HeadingLevel::H2)) => {
+                    current_section = mem::take(&mut text);
+                }
+                Event::End(TagEnd::Heading(level)) => {
+                    anyhow::bail!("Unexpected heading level: {level}");
+                }
+                Event::Start(Tag::CodeBlock(kind)) => {
+                    match kind {
+                        CodeBlockKind::Fenced(info) => {
+                            block_info = info;
+                        }
+                        CodeBlockKind::Indented => {
+                            anyhow::bail!("Unexpected indented codeblock");
+                        }
+                    };
+                }
+                Event::Start(_) => {
+                    text.clear();
+                    block_info = "".into();
+                }
+                Event::End(TagEnd::CodeBlock) => {
+                    if current_section.eq_ignore_ascii_case(EDIT_HISTORY_HEADING) {
+                        named.example.edit_history.push(mem::take(&mut text));
+                    } else if current_section.eq_ignore_ascii_case(CURSOR_POSITION_HEADING) {
+                        let path = PathBuf::from(block_info.trim());
+                        named.example.cursor_path = path;
+                        named.example.cursor_position = mem::take(&mut text);
+                    } else if current_section.eq_ignore_ascii_case(EXPECTED_PATCH_HEADING) {
+                        named.example.expected_patch = mem::take(&mut text);
+                    } else if current_section.eq_ignore_ascii_case(EXPECTED_EXCERPTS_HEADING) {
+                        let path = PathBuf::from(block_info.trim());
+                        named.example.expected_excerpts.push(ExpectedExcerpt {
+                            path,
+                            text: mem::take(&mut text),
+                        });
+                    } else {
+                        eprintln!("Warning: Unrecognized section `{current_section:?}`")
+                    }
+                }
+                _ => {}
+            }
+        }
+
+        if named.example.cursor_path.as_path() == Path::new("")
+            || named.example.cursor_position.is_empty()
+        {
+            anyhow::bail!("Missing cursor position codeblock");
+        }
+
+        Ok(named)
+    }
+
+    pub fn write(&self, format: ExampleFormat, mut out: impl Write) -> Result<()> {
+        match format {
+            ExampleFormat::Json => Ok(serde_json::to_writer(out, &self.example)?),
+            ExampleFormat::Toml => {
+                Ok(out.write_all(toml::to_string_pretty(&self.example)?.as_bytes())?)
+            }
+            ExampleFormat::Md => Ok(write!(out, "{}", self)?),
+        }
+    }
+
+    #[allow(unused)]
+    pub async fn setup_worktree(&self) -> Result<PathBuf> {
+        let worktrees_dir = env::current_dir()?.join("target").join("zeta-worktrees");
+        let repos_dir = env::current_dir()?.join("target").join("zeta-repos");
+        fs::create_dir_all(&repos_dir)?;
+        fs::create_dir_all(&worktrees_dir)?;
+
+        let (repo_owner, repo_name) = self.repo_name()?;
+
+        let repo_dir = repos_dir.join(repo_owner.as_ref()).join(repo_name.as_ref());
+        if !repo_dir.is_dir() {
+            fs::create_dir_all(&repo_dir)?;
+            run_git(&repo_dir, &["init"]).await?;
+            run_git(
+                &repo_dir,
+                &["remote", "add", "origin", &self.example.repository_url],
+            )
+            .await?;
+        }
+
+        run_git(
+            &repo_dir,
+            &["fetch", "--depth", "1", "origin", &self.example.revision],
+        )
+        .await?;
+
+        let worktree_path = worktrees_dir.join(&self.name);
+
+        if worktree_path.is_dir() {
+            run_git(&worktree_path, &["clean", "--force", "-d"]).await?;
+            run_git(&worktree_path, &["reset", "--hard", "HEAD"]).await?;
+            run_git(&worktree_path, &["checkout", &self.example.revision]).await?;
+        } else {
+            let worktree_path_string = worktree_path.to_string_lossy();
+            run_git(
+                &repo_dir,
+                &[
+                    "worktree",
+                    "add",
+                    "-f",
+                    &worktree_path_string,
+                    &self.example.revision,
+                ],
+            )
+            .await?;
+        }
+
+        Ok(worktree_path)
+    }
+
+    #[allow(unused)]
+    fn repo_name(&self) -> Result<(Cow<'_, str>, Cow<'_, str>)> {
+        // git@github.com:owner/repo.git
+        if self.example.repository_url.contains('@') {
+            let (owner, repo) = self
+                .example
+                .repository_url
+                .split_once(':')
+                .context("expected : in git url")?
+                .1
+                .split_once('/')
+                .context("expected / in git url")?;
+            Ok((
+                Cow::Borrowed(owner),
+                Cow::Borrowed(repo.trim_end_matches(".git")),
+            ))
+        // http://github.com/owner/repo.git
+        } else {
+            let url = Url::parse(&self.example.repository_url)?;
+            let mut segments = url.path_segments().context("empty http url")?;
+            let owner = segments
+                .next()
+                .context("expected owner path segment")?
+                .to_string();
+            let repo = segments
+                .next()
+                .context("expected repo path segment")?
+                .trim_end_matches(".git")
+                .to_string();
+            assert!(segments.next().is_none());
+
+            Ok((owner.into(), repo.into()))
+        }
+    }
+}
+
+async fn run_git(repo_path: &Path, args: &[&str]) -> Result<String> {
+    let output = smol::process::Command::new("git")
+        .current_dir(repo_path)
+        .args(args)
+        .output()
+        .await?;
+
+    anyhow::ensure!(
+        output.status.success(),
+        "`git {}` within `{}` failed with status: {}\nstderr:\n{}\nstdout:\n{}",
+        args.join(" "),
+        repo_path.display(),
+        output.status,
+        String::from_utf8_lossy(&output.stderr),
+        String::from_utf8_lossy(&output.stdout),
+    );
+    Ok(String::from_utf8(output.stdout)?.trim().to_string())
+}
+
+impl Display for NamedExample {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "# {}\n\n", self.name)?;
+        write!(
+            f,
+            "{REPOSITORY_URL_FIELD} = {}\n",
+            self.example.repository_url
+        )?;
+        write!(f, "{REVISION_FIELD} = {}\n\n", self.example.revision)?;
+
+        write!(
+            f,
+            "## {CURSOR_POSITION_HEADING}\n\n`````{}\n{}`````\n",
+            self.example.cursor_path.display(),
+            self.example.cursor_position
+        )?;
+        write!(f, "## {EDIT_HISTORY_HEADING}\n\n")?;
+
+        if !self.example.edit_history.is_empty() {
+            write!(f, "`````diff\n")?;
+            for item in &self.example.edit_history {
+                write!(f, "{item}")?;
+            }
+            write!(f, "`````\n")?;
+        }
+
+        if !self.example.expected_patch.is_empty() {
+            write!(
+                f,
+                "\n## {EXPECTED_PATCH_HEADING}\n\n`````diff\n{}`````\n",
+                self.example.expected_patch
+            )?;
+        }
+
+        if !self.example.expected_excerpts.is_empty() {
+            write!(f, "\n## {EXPECTED_EXCERPTS_HEADING}\n\n")?;
+
+            for excerpt in &self.example.expected_excerpts {
+                write!(
+                    f,
+                    "`````{}{}\n{}`````\n\n",
+                    excerpt
+                        .path
+                        .extension()
+                        .map(|ext| format!("{} ", ext.to_string_lossy()))
+                        .unwrap_or_default(),
+                    excerpt.path.display(),
+                    excerpt.text
+                )?;
+            }
+        }
+
+        Ok(())
+    }
+}

crates/zeta_cli/src/main.rs πŸ”—

@@ -1,8 +1,10 @@
+mod example;
 mod headless;
 mod source_location;
 mod syntax_retrieval_stats;
 mod util;
 
+use crate::example::{ExampleFormat, NamedExample};
 use crate::syntax_retrieval_stats::retrieval_stats;
 use ::serde::Serialize;
 use ::util::paths::PathStyle;
@@ -22,6 +24,7 @@ use language_model::LanguageModelRegistry;
 use project::{Project, Worktree};
 use reqwest_client::ReqwestClient;
 use serde_json::json;
+use std::io;
 use std::{collections::HashSet, path::PathBuf, process::exit, str::FromStr, sync::Arc};
 use zeta2::{ContextMode, LlmContextOptions, SearchToolQuery};
 
@@ -48,6 +51,11 @@ enum Command {
         #[command(subcommand)]
         command: Zeta2Command,
     },
+    ConvertExample {
+        path: PathBuf,
+        #[arg(long, value_enum, default_value_t = ExampleFormat::Md)]
+        output_format: ExampleFormat,
+    },
 }
 
 #[derive(Subcommand, Debug)]
@@ -641,6 +649,15 @@ fn main() {
                         }
                     },
                 },
+                Command::ConvertExample {
+                    path,
+                    output_format,
+                } => {
+                    let example = NamedExample::load(path).unwrap();
+                    example.write(output_format, io::stdout()).unwrap();
+                    let _ = cx.update(|cx| cx.quit());
+                    return;
+                }
             };
 
             match result {

docs/src/SUMMARY.md πŸ”—

@@ -165,6 +165,5 @@
   - [Local Collaboration](./development/local-collaboration.md)
   - [Using Debuggers](./development/debuggers.md)
   - [Glossary](./development/glossary.md)
-- [Release Process](./development/releases.md)
 - [Release Notes](./development/release-notes.md)
 - [Debugging Crashes](./development/debugging-crashes.md)

docs/src/development.md πŸ”—

@@ -88,7 +88,6 @@ in-depth examples and explanations.
 ## Contributor links
 
 - [CONTRIBUTING.md](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md)
-- [Releases](./development/releases.md)
 - [Debugging Crashes](./development/debugging-crashes.md)
 - [Code of Conduct](https://zed.dev/code-of-conduct)
 - [Zed Contributor License](https://zed.dev/cla)

docs/src/development/releases.md πŸ”—

@@ -1,147 +0,0 @@
-# Zed Releases
-
-Read about Zed's [release channels here](https://zed.dev/faq#what-are-the-release-channels).
-
-## Wednesday Release Process
-
-You will need write access to the Zed repository to do this.
-
-Credentials for various services used in this process can be found in 1Password.
-
-Use the `releases` Slack channel to notify the team that releases will be starting.
-This is mostly a formality on Wednesday's minor update releases, but can be beneficial when doing patch releases, as other devs may have landed fixes they'd like to cherry pick.
-
-### Starting the Builds
-
-1. Checkout `main` and ensure your working copy is clean.
-
-1. Run `git fetch && git pull` to ensure you have the latest commits locally.
-
-1. Run `git fetch --tags --force` to forcibly ensure your local tags are in sync with the remote.
-
-1. Run `./script/get-stable-channel-release-notes` and store output locally.
-
-1. Run `./script/bump-zed-minor-versions`.
-
-   - Push the tags and branches as instructed.
-
-1. Run `./script/get-preview-channel-changes` and store output locally.
-
-> **Note:** Always prioritize the stable release.
-> If you've completed aggregating stable release notes, you can move on to working on aggregating preview release notes, but once the stable build has finished, work through the rest of the stable steps to fully publish.
-> Preview can be finished up after.
-
-### Stable Release
-
-1. Aggregate stable release notes.
-
-   - Follow the instructions at the end of the script and aggregate the release notes into one structure.
-
-1. Once the stable release draft is up on [GitHub Releases](https://github.com/zed-industries/zed/releases), paste the stable release notes into it and **save**.
-
-   - **Do not publish the draft!**
-
-1. Check the stable release assets.
-
-   - Ensure the stable release job has finished without error.
-   - Ensure the draft has the proper number of assetsβ€”releases currently have 12 assets each (as of v0.211).
-   - Download the artifacts for the stable release draft and test that you can run them locally.
-
-1. Publish the stable draft on [GitHub Releases](https://github.com/zed-industries/zed/releases).
-
-   - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild.
-     The release will be public once the rebuild has completed.
-
-1. Post the stable release notes to social media.
-
-   - Bluesky and X posts will already be built as drafts in [Buffer](https://buffer.com).
-   - Double-check links.
-   - Publish both, one at a time, ensuring both are posted to each respective platform.
-
-1. Send the stable release notes email.
-
-   - The email broadcast will already be built as a draft in [Kit](https://kit.com).
-   - Double-check links.
-   - Publish the email.
-
-### Preview Release
-
-1. Aggregate preview release notes.
-
-   - Take the script's output and build release notes by organizing each release note line into a category.
-   - Use a prior release for the initial outline.
-   - Make sure to append the `Credit` line, if present, to the end of each release note line.
-
-1. Once the preview release draft is up on [GitHub Releases](https://github.com/zed-industries/zed/releases), paste the preview release notes into it and **save**.
-
-   - **Do not publish the draft!**
-
-1. Check the preview release assets.
-
-   - Ensure the preview release job has finished without error.
-   - Ensure the draft has the proper number of assetsβ€”releases currently have 12 assets each (as of v0.211).
-   - Download the artifacts for the preview release draft and test that you can run them locally.
-
-1. Publish the preview draft on [GitHub Releases](https://github.com/zed-industries/zed/releases).
-   - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild.
-     The release will be public once the rebuild has completed.
-
-### Prep Content for Next Week's Stable Release
-
-1. Build social media posts based on the popular items in preview.
-
-   - Draft the copy in the [tweets](https://zed.dev/channel/tweets-23331) channel.
-   - Create the preview media (videos, screenshots).
-     - For features that you film videos around, try to create alternative photo-only versions to be used in the email, as videos and GIFs aren't great for email.
-     - Store all created media in `Feature Media` in our Google Drive.
-   - Build X and Bluesky post drafts (copy and media) in [Buffer](https://buffer.com), to be sent for next week's stable release.
-
-   **Note: These are preview items and you may discover bugs.**
-   **This is a very good time to report these findings to the team!**
-
-1. Build email based on the popular items in preview.
-
-   - You can reuse the copy and photo media from the preview social media posts.
-   - Create a draft email in [Kit](https://kit.com), to be sent for next week's stable release.
-
-## Patch Release Process
-
-If your PR fixes a panic or a crash, you should cherry-pick it to the current stable and preview branches.
-If your PR fixes a regression in recently released code, you should cherry-pick it to preview.
-
-You will need write access to the Zed repository to do this:
-
----
-
-1. Send a PR containing your change to `main` as normal.
-
-1. Once it is merged, cherry-pick the commit locally to either of the release branches (`v0.XXX.x`).
-
-   - In some cases, you may have to handle a merge conflict.
-     More often than not, this will happen when cherry-picking to stable, as the stable branch is more "stale" than the preview branch.
-
-1. After the commit is cherry-picked, run `./script/trigger-release {preview|stable}`.
-   This will bump the version numbers, create a new release tag, and kick off a release build.
-
-   - This can also be run from the [GitHub Actions UI](https://github.com/zed-industries/zed/actions/workflows/bump_patch_version.yml):
-     ![](https://github.com/zed-industries/zed/assets/1486634/9e31ae95-09e1-4c7f-9591-944f4f5b63ea)
-
-1. Once release drafts are up on [GitHub Releases](https://github.com/zed-industries/zed/releases), proofread and edit the release notes as needed and **save**.
-
-   - **Do not publish the drafts, yet.**
-
-1. Check the release assets.
-
-   - Ensure the stable / preview release jobs have finished without error.
-   - Ensure each draft has the proper number of assetsβ€”releases currently have 10 assets each.
-   - Download the artifacts for each release draft and test that you can run them locally.
-
-1. Publish stable / preview drafts, one at a time.
-   - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild.
-     The release will be public once the rebuild has completed.
-
-## Nightly release process
-
-In addition to the public releases, we also have a nightly build that we encourage employees to use.
-Nightly is released by cron once a day, and can be shipped as often as you'd like.
-There are no release notes or announcements, so you can just merge your changes to main and run `./script/trigger-release nightly`.

docs/src/extensions/icon-themes.md πŸ”—

@@ -11,7 +11,7 @@ The [Material Icon Theme](https://github.com/zed-extensions/material-icon-theme)
 There are two important directories for an icon theme extension:
 
 - `icon_themes`: This directory will contain one or more JSON files containing the icon theme definitions.
-- `icons`: This directory contains the icons assets that will be distributed with the extension. You can created subdirectories in this directory, if so desired.
+- `icons`: This directory contains the icon assets that will be distributed with the extension. You can created subdirectories in this directory, if so desired.
 
 Each icon theme file should adhere to the JSON schema specified at [`https://zed.dev/schema/icon_themes/v0.3.0.json`](https://zed.dev/schema/icon_themes/v0.3.0.json).
 

docs/src/extensions/languages.md πŸ”—

@@ -324,7 +324,7 @@ This query marks number and string values in key-value pairs and arrays for reda
 
 The `runnables.scm` file defines rules for detecting runnable code.
 
-Here's an example from an `runnables.scm` file for JSON:
+Here's an example from a `runnables.scm` file for JSON:
 
 ```scheme
 (

docs/src/icon-themes.md πŸ”—

@@ -4,19 +4,21 @@ Zed comes with a built-in icon theme, with more icon themes available as extensi
 
 ## Selecting an Icon Theme
 
-See what icon themes are installed and preview them via the Icon Theme Selector, which you can open from the command palette with "icon theme selector: toggle".
+See what icon themes are installed and preview them via the Icon Theme Selector, which you can open from the command palette with `icon theme selector: toggle`.
 
 Navigating through the icon theme list by moving up and down will change the icon theme in real time and hitting enter will save it to your settings file.
 
 ## Installing more Icon Themes
 
-More icon themes are available from the Extensions page, which you can access via the command palette with `zed: extensions` or the [Zed website](https://zed.dev/extensions).
+More icon themes are available from the Extensions page, which you can access via the command palette with `zed: extensions` or the [Zed website](https://zed.dev/extensions?filter=icon-themes).
 
 ## Configuring Icon Themes
 
-Your selected icon theme is stored in your settings file. You can open your settings file from the command palette with `zed: open settings file` (bound to `cmd-alt-,` on macOS and `ctrl-alt-,` on Linux).
+Your selected icon theme is stored in your settings file.
+You can open your settings file from the command palette with {#action zed::OpenSettingsFile} (bound to {#kb zed::OpenSettingsFile}).
 
-Just like with themes, Zed allows for configuring different icon themes for light and dark mode. You can set the mode to `"light"` or `"dark"` to ignore the current system mode.
+Just like with themes, Zed allows for configuring different icon themes for light and dark mode.
+You can set the mode to `"light"` or `"dark"` to ignore the current system mode.
 
 ```json [settings]
 {

docs/src/languages/rego.md πŸ”—

@@ -7,7 +7,7 @@ Rego language support in Zed is provided by the community-maintained [Rego exten
 
 ## Installation
 
-The extensions is largely based on the [Regal](https://docs.styra.com/regal/language-server) language server which should be installed to make use of the extension. Read the [getting started](https://docs.styra.com/regal#getting-started) instructions for more information.
+The extension is largely based on the [Regal](https://docs.styra.com/regal/language-server) language server which should be installed to make use of the extension. Read the [getting started](https://docs.styra.com/regal#getting-started) instructions for more information.
 
 ## Configuration
 

docs/src/snippets.md πŸ”—

@@ -1,6 +1,6 @@
 # Snippets
 
-Use the {#action snippets::ConfigureSnippets} action to create a new snippets file or edit a existing snippets file for a specified [scope](#scopes).
+Use the {#action snippets::ConfigureSnippets} action to create a new snippets file or edit an existing snippets file for a specified [scope](#scopes).
 
 The snippets are located in `~/.config/zed/snippets` directory to which you can navigate to with the {#action snippets::OpenFolder} action.
 

docs/src/themes.md πŸ”—

@@ -4,21 +4,23 @@ Zed comes with a number of built-in themes, with more themes available as extens
 
 ## Selecting a Theme
 
-See what themes are installed and preview them via the Theme Selector, which you can open from the command palette with "theme selector: Toggle" (bound to `cmd-k cmd-t` on macOS and `ctrl-k ctrl-t` on Linux).
+See what themes are installed and preview them via the Theme Selector, which you can open from the command palette with `theme selector: toggle` (bound to {#kb theme_selector::Toggle}).
 
 Navigating through the theme list by moving up and down will change the theme in real time and hitting enter will save it to your settings file.
 
 ## Installing more Themes
 
-More themes are available from the Extensions page, which you can access via the command palette with `zed: extensions` or the [Zed website](https://zed.dev/extensions).
+More themes are available from the Extensions page, which you can access via the command palette with `zed: extensions` or the [Zed website](https://zed.dev/extensions?filter=themes).
 
 Many popular themes have been ported to Zed, and if you're struggling to choose one, visit [zed-themes.com](https://zed-themes.com), a third-party gallery with visible previews for many of them.
 
 ## Configuring a Theme
 
-Your selected theme is stored in your settings file. You can open your settings file from the command palette with `zed: open settings file` (bound to `cmd-alt-,` on macOS and `ctrl-alt-,` on Linux).
+Your selected theme is stored in your settings file.
+You can open your settings file from the command palette with {#action zed::OpenSettingsFile} (bound to {#kb zed::OpenSettingsFile}).
 
-By default, Zed maintains two themes: one for light mode and one for dark mode. You can set the mode to `"dark"` or `"light"` to ignore the current system mode.
+By default, Zed maintains two themes: one for light mode and one for dark mode.
+You can set the mode to `"dark"` or `"light"` to ignore the current system mode.
 
 ```json [settings]
 {
@@ -32,7 +34,8 @@ By default, Zed maintains two themes: one for light mode and one for dark mode.
 
 ## Theme Overrides
 
-To override specific attributes of a theme, use the `theme_overrides` setting. This setting can be used to configure theme-specific overrides.
+To override specific attributes of a theme, use the `theme_overrides` setting.
+This setting can be used to configure theme-specific overrides.
 
 For example, add the following to your `settings.json` if you wish to override the background color of the editor and display comments and doc comments as italics:
 
@@ -54,17 +57,17 @@ For example, add the following to your `settings.json` if you wish to override t
 }
 ```
 
-To see a comprehensive list of list of captures (like `comment` and `comment.doc`) see: [Language Extensions: Syntax highlighting](./extensions/languages.md#syntax-highlighting).
+To see a comprehensive list of list of captures (like `comment` and `comment.doc`) see [Language Extensions: Syntax highlighting](./extensions/languages.md#syntax-highlighting).
 
-To see a list of available theme attributes look at the JSON file for your theme. For example, [assets/themes/one/one.json](https://github.com/zed-industries/zed/blob/main/assets/themes/one/one.json) for the default One Dark and One Light themes.
+To see a list of available theme attributes look at the JSON file for your theme.
+For example, [assets/themes/one/one.json](https://github.com/zed-industries/zed/blob/main/assets/themes/one/one.json) for the default One Dark and One Light themes.
 
 ## Local Themes
 
 Store new themes locally by placing them in the `~/.config/zed/themes` directory (macOS and Linux) or `%USERPROFILE%\AppData\Roaming\Zed\themes\` (Windows).
 
-For example, to create a new theme called `my-cool-theme`, create a file called `my-cool-theme.json` in that directory. It will be available in the theme selector the next time Zed loads.
-
-Find more themes at [zed-themes.com](https://zed-themes.com).
+For example, to create a new theme called `my-cool-theme`, create a file called `my-cool-theme.json` in that directory.
+It will be available in the theme selector the next time Zed loads.
 
 ## Theme Development
 

docs/src/visual-customization.md πŸ”—

@@ -1,14 +1,14 @@
 # Visual Customization
 
-Various aspects of Zed's visual layout can be configured via Zed settings.json which you can access via {#action zed::OpenSettings} ({#kb zed::OpenSettings}).
+Various aspects of Zed's visual layout can be configured via either the settings window or the `settings.json` file, which you can access via {#action zed::OpenSettings} ({#kb zed::OpenSettings}) and {#action zed::OpenSettingsFile} ({#kb zed::OpenSettingsFile}) respectively.
 
 See [Configuring Zed](./configuring-zed.md) for additional information and other non-visual settings.
 
 ## Themes
 
-Use may install zed extensions providing [Themes](./themes.md) and [Icon Themes](./icon-themes.md) via {#action zed::Extensions} from the command palette or menu.
+You can install many [themes](./themes.md) and [icon themes](./icon-themes.md) in form of extensions by running {#action zed::Extensions} from the command palette.
 
-You can preview/choose amongst your installed themes and icon themes with {#action theme_selector::Toggle} ({#kb theme_selector::Toggle}) and ({#action icon_theme_selector::Toggle}) which will modify the following settings:
+You can preview/choose amongst your installed themes and icon themes with {#action theme_selector::Toggle} ({#kb theme_selector::Toggle}) and {#action icon_theme_selector::Toggle} ({#kb icon_theme_selector::Toggle}) which will modify the following settings:
 
 ```json [settings]
 {
@@ -61,15 +61,20 @@ If you would like to use distinct themes for light mode/dark mode that can be se
     "line_height": "standard",
   },
 
-  // Agent Panel Font Settings
-  "agent_font_size": 15
+  // Controls the font size for agent responses in the agent panel.
+  // If not specified, it falls back to the UI font size.
+  "agent_ui_font_size": 15,
+  // Controls the font size for the agent panel's message editor, user message,
+  // and any other snippet of code.
+  "agent_buffer_font_size": 12
 ```
 
 ### Font ligatures
 
 By default Zed enable font ligatures which will visually combines certain adjacent characters.
 
-For example `=>` will be displayed as `β†’` and `!=` will be `β‰ `. This is purely cosmetic and the individual characters remain unchanged.
+For example `=>` will be displayed as `β†’` and `!=` will be `β‰ `.
+This is purely cosmetic and the individual characters remain unchanged.
 
 To disable this behavior use:
 
@@ -464,7 +469,12 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k
     "default_width": 640,   // Default width (left/right docked)
     "default_height": 320,  // Default height (bottom docked)
   },
-  "agent_font_size": 16
+  // Controls the font size for agent responses in the agent panel.
+  // If not specified, it falls back to the UI font size.
+  "agent_ui_font_size": 15,
+  // Controls the font size for the agent panel's message editor, user message,
+  // and any other snippet of code.
+  "agent_buffer_font_size": 12
 ```
 
 See [Zed AI Documentation](./ai/overview.md) for additional non-visual AI settings.

script/run-unit-evals πŸ”—

@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+set -euxo pipefail
+
+cargo nextest run --workspace --no-fail-fast --features unit-eval --no-capture -E 'test(::eval_)'

tooling/xtask/src/tasks/workflows.rs πŸ”—

@@ -10,6 +10,7 @@ mod release_nightly;
 mod run_bundling;
 
 mod release;
+mod run_agent_evals;
 mod run_tests;
 mod runners;
 mod steps;
@@ -28,6 +29,8 @@ pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> {
         ("run_tests.yml", run_tests::run_tests()),
         ("release.yml", release::release()),
         ("compare_perf.yml", compare_perf::compare_perf()),
+        ("run_unit_evals.yml", run_agent_evals::run_unit_evals()),
+        ("run_agent_evals.yml", run_agent_evals::run_agent_evals()),
     ];
     fs::create_dir_all(dir)
         .with_context(|| format!("Failed to create directory: {}", dir.display()))?;

tooling/xtask/src/tasks/workflows/run_agent_evals.rs πŸ”—

@@ -0,0 +1,113 @@
+use gh_workflow::{
+    Event, Expression, Job, PullRequest, PullRequestType, Run, Schedule, Step, Use, Workflow,
+    WorkflowDispatch,
+};
+
+use crate::tasks::workflows::{
+    runners::{self, Platform},
+    steps::{self, FluentBuilder as _, NamedJob, named, setup_cargo_config},
+    vars,
+};
+
+pub(crate) fn run_agent_evals() -> Workflow {
+    let agent_evals = agent_evals();
+
+    named::workflow()
+        .on(Event::default()
+            .schedule([Schedule::default().cron("0 0 * * *")])
+            .pull_request(PullRequest::default().add_branch("**").types([
+                PullRequestType::Synchronize,
+                PullRequestType::Reopened,
+                PullRequestType::Labeled,
+            ]))
+            .workflow_dispatch(WorkflowDispatch::default()))
+        .concurrency(vars::one_workflow_per_non_main_branch())
+        .add_env(("CARGO_TERM_COLOR", "always"))
+        .add_env(("CARGO_INCREMENTAL", 0))
+        .add_env(("RUST_BACKTRACE", 1))
+        .add_env(("ANTHROPIC_API_KEY", "${{ secrets.ANTHROPIC_API_KEY }}"))
+        .add_env((
+            "ZED_CLIENT_CHECKSUM_SEED",
+            "${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}",
+        ))
+        .add_env(("ZED_EVAL_TELEMETRY", 1))
+        .add_job(agent_evals.name, agent_evals.job)
+}
+
+fn agent_evals() -> NamedJob {
+    fn run_eval() -> Step<Run> {
+        named::bash("cargo run --package=eval -- --repetitions=8 --concurrency=1")
+    }
+
+    named::job(
+        Job::default()
+            .cond(Expression::new(indoc::indoc!{r#"
+                github.repository_owner == 'zed-industries' &&
+                (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
+            "#}))
+            .runs_on(runners::LINUX_DEFAULT)
+            .timeout_minutes(60_u32)
+            .add_step(steps::checkout_repo())
+            .add_step(steps::cache_rust_dependencies())
+            .map(steps::install_linux_dependencies)
+            .add_step(setup_cargo_config(Platform::Linux))
+            .add_step(steps::script("cargo build --package=eval"))
+            .add_step(run_eval())
+            .add_step(steps::cleanup_cargo_config(Platform::Linux))
+    )
+}
+
+pub(crate) fn run_unit_evals() -> Workflow {
+    let unit_evals = unit_evals();
+
+    named::workflow()
+        .on(Event::default()
+            .schedule([
+                // GitHub might drop jobs at busy times, so we choose a random time in the middle of the night.
+                Schedule::default().cron("47 1 * * 2"),
+            ])
+            .workflow_dispatch(WorkflowDispatch::default()))
+        .concurrency(vars::one_workflow_per_non_main_branch())
+        .add_env(("CARGO_TERM_COLOR", "always"))
+        .add_env(("CARGO_INCREMENTAL", 0))
+        .add_env(("RUST_BACKTRACE", 1))
+        .add_env((
+            "ZED_CLIENT_CHECKSUM_SEED",
+            "${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}",
+        ))
+        .add_job(unit_evals.name, unit_evals.job)
+}
+
+fn unit_evals() -> NamedJob {
+    fn send_failure_to_slack() -> Step<Use> {
+        named::uses(
+            "slackapi",
+            "slack-github-action",
+            "b0fa283ad8fea605de13dc3f449259339835fc52",
+        )
+        .if_condition(Expression::new("${{ failure() }}"))
+        .add_with(("method", "chat.postMessage"))
+        .add_with(("token", "${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}"))
+        .add_with(("payload", indoc::indoc!{r#"
+            channel: C04UDRNNJFQ
+            text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
+        "#}))
+    }
+
+    named::job(
+        Job::default()
+            .runs_on(runners::LINUX_DEFAULT)
+            .add_step(steps::checkout_repo())
+            .add_step(steps::setup_cargo_config(Platform::Linux))
+            .add_step(steps::cache_rust_dependencies())
+            .map(steps::install_linux_dependencies)
+            .add_step(steps::cargo_install_nextest(Platform::Linux))
+            .add_step(steps::clear_target_dir_if_large(Platform::Linux))
+            .add_step(
+                steps::script("./script/run-unit-evals")
+                    .add_env(("ANTHROPIC_API_KEY", "${{ secrets.ANTHROPIC_API_KEY }}")),
+            )
+            .add_step(send_failure_to_slack())
+            .add_step(steps::cleanup_cargo_config(Platform::Linux)),
+    )
+}