Merge branch 'main' into zeta2-fix-ctx-refresh

Agus Zubiaga created

Change summary

.github/workflows/ci.yml                             |   7 
.github/workflows/release_nightly.yml                |  57 +
.github/workflows/run_bundling.yml                   |  53 +
Cargo.lock                                           |   1 
assets/keymaps/default-linux.json                    |   7 
assets/keymaps/default-macos.json                    |   7 
assets/keymaps/default-windows.json                  |   7 
crates/zeta2/src/related_excerpts.rs                 |  65 +
crates/zeta2/src/zeta2.rs                            |  85 ++
crates/zeta2_tools/Cargo.toml                        |   1 
crates/zeta2_tools/src/zeta2_context_view.rs         | 412 ++++++++++++++
crates/zeta2_tools/src/zeta2_tools.rs                |  39 +
docs/src/configuring-zed.md                          |   2 
docs/src/installation.md                             |   4 
script/bundle-mac                                    | 119 +--
script/upload-nightly                                |  40 
tooling/xtask/src/tasks/workflows/release_nightly.rs |  40 
tooling/xtask/src/tasks/workflows/run_bundling.rs    |  20 
18 files changed, 792 insertions(+), 174 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -563,8 +563,11 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
-      - name: Create macOS app bundle
-        run: script/bundle-mac
+      - name: Create macOS app bundle (aarch64)
+        run: script/bundle-mac aarch64-apple-darwin
+
+      - name: Create macOS app bundle (x64)
+        run: script/bundle-mac x86_64-apple-darwin
 
       - name: Rename binaries
         run: |

.github/workflows/release_nightly.yml 🔗

@@ -96,7 +96,7 @@ jobs:
         Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue
       shell: pwsh
     timeout-minutes: 60
-  bundle_mac_nightly:
+  bundle_mac_nightly_x86_64:
     needs:
     - check_style
     - run_tests_mac
@@ -131,11 +131,53 @@ jobs:
         echo "Publishing version: ${version} on release channel nightly"
         echo "nightly" > crates/zed/RELEASE_CHANNEL
       shell: bash -euxo pipefail {0}
-    - name: ./script/bundle-mac
-      run: ./script/bundle-mac
+    - name: run_bundling::bundle_mac
+      run: ./script/bundle-mac x86_64-apple-darwin
       shell: bash -euxo pipefail {0}
     - name: release_nightly::upload_zed_nightly
-      run: script/upload-nightly macos
+      run: script/upload-nightly macos x86_64
+      shell: bash -euxo pipefail {0}
+    timeout-minutes: 60
+  bundle_mac_nightly_aarch64:
+    needs:
+    - check_style
+    - run_tests_mac
+    if: github.repository_owner == 'zed-industries'
+    runs-on: self-mini-macos
+    env:
+      MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+      MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+      APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
+      APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
+      APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
+    steps:
+    - name: steps::checkout_repo
+      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      with:
+        clean: false
+    - name: steps::setup_node
+      uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+      with:
+        node-version: '20'
+    - name: steps::setup_sentry
+      uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
+      with:
+        token: ${{ secrets.SENTRY_AUTH_TOKEN }}
+    - name: steps::clear_target_dir_if_large
+      run: ./script/clear-target-dir-if-larger-than 300
+      shell: bash -euxo pipefail {0}
+    - name: release_nightly::set_release_channel_to_nightly
+      run: |
+        set -eu
+        version=$(git rev-parse --short HEAD)
+        echo "Publishing version: ${version} on release channel nightly"
+        echo "nightly" > crates/zed/RELEASE_CHANNEL
+      shell: bash -euxo pipefail {0}
+    - name: run_bundling::bundle_mac
+      run: ./script/bundle-mac aarch64-apple-darwin
+      shell: bash -euxo pipefail {0}
+    - name: release_nightly::upload_zed_nightly
+      run: script/upload-nightly macos aarch64
       shell: bash -euxo pipefail {0}
     timeout-minutes: 60
   bundle_linux_nightly_x86_64:
@@ -176,7 +218,7 @@ jobs:
       run: ./script/bundle-linux
       shell: bash -euxo pipefail {0}
     - name: release_nightly::upload_zed_nightly
-      run: script/upload-nightly linux-targz
+      run: script/upload-nightly linux-targz x86_64
       shell: bash -euxo pipefail {0}
     timeout-minutes: 60
   bundle_linux_nightly_aarch64:
@@ -214,7 +256,7 @@ jobs:
       run: ./script/bundle-linux
       shell: bash -euxo pipefail {0}
     - name: release_nightly::upload_zed_nightly
-      run: script/upload-nightly linux-targz
+      run: script/upload-nightly linux-targz aarch64
       shell: bash -euxo pipefail {0}
     timeout-minutes: 60
   bundle_windows_nightly_x86_64:
@@ -372,7 +414,8 @@ jobs:
     continue-on-error: true
   update_nightly_tag:
     needs:
-    - bundle_mac_nightly
+    - bundle_mac_nightly_x86_64
+    - bundle_mac_nightly_aarch64
     - bundle_linux_nightly_x86_64
     - bundle_linux_nightly_aarch64
     - bundle_windows_nightly_x86_64

.github/workflows/run_bundling.yml 🔗

@@ -13,7 +13,7 @@ on:
     - labeled
     - synchronize
 jobs:
-  bundle_mac:
+  bundle_mac_x86_64:
     if: |-
       (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
                        (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
@@ -40,19 +40,60 @@ jobs:
     - name: steps::clear_target_dir_if_large
       run: ./script/clear-target-dir-if-larger-than 300
       shell: bash -euxo pipefail {0}
-    - name: ./script/bundle-mac
-      run: ./script/bundle-mac
+    - name: run_bundling::bundle_mac
+      run: ./script/bundle-mac x86_64-apple-darwin
+      shell: bash -euxo pipefail {0}
+    - name: '@actions/upload-artifact Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg'
+      uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
+      with:
+        name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
+        path: target/x86_64-apple-darwin/release/Zed.dmg
+    - name: '@actions/upload-artifact zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-macos-x86_64.gz'
+      uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
+      with:
+        name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-macos-x86_64.gz
+        path: target/zed-remote-server-macos-x86_64.gz
+    timeout-minutes: 60
+  bundle_mac_arm64:
+    if: |-
+      (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
+                       (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
+    runs-on: self-mini-macos
+    env:
+      MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+      MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
+      APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
+      APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
+      APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
+    steps:
+    - name: steps::checkout_repo
+      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      with:
+        clean: false
+    - name: steps::setup_node
+      uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
+      with:
+        node-version: '20'
+    - name: steps::setup_sentry
+      uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
+      with:
+        token: ${{ secrets.SENTRY_AUTH_TOKEN }}
+    - name: steps::clear_target_dir_if_large
+      run: ./script/clear-target-dir-if-larger-than 300
+      shell: bash -euxo pipefail {0}
+    - name: run_bundling::bundle_mac
+      run: ./script/bundle-mac aarch64-apple-darwin
       shell: bash -euxo pipefail {0}
     - name: '@actions/upload-artifact Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg'
       uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
       with:
         name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
         path: target/aarch64-apple-darwin/release/Zed.dmg
-    - name: '@actions/upload-artifact Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg'
+    - name: '@actions/upload-artifact zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-macos-aarch64.gz'
       uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
       with:
-        name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
-        path: target/x86_64-apple-darwin/release/Zed.dmg
+        name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-macos-aarch64.gz
+        path: target/zed-remote-server-macos-aarch64.gz
     timeout-minutes: 60
   bundle_linux_x86_64:
     if: |-

Cargo.lock 🔗

@@ -21670,6 +21670,7 @@ dependencies = [
 name = "zeta2_tools"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "chrono",
  "clap",
  "client",

assets/keymaps/default-linux.json 🔗

@@ -1298,5 +1298,12 @@
       "ctrl-enter up": "dev::Zeta2RatePredictionPositive",
       "ctrl-enter down": "dev::Zeta2RatePredictionNegative"
     }
+  },
+  {
+    "context": "Zeta2Context > Editor",
+    "bindings": {
+      "alt-left": "dev::Zeta2ContextGoBack",
+      "alt-right": "dev::Zeta2ContextGoForward"
+    }
   }
 ]

assets/keymaps/default-macos.json 🔗

@@ -1404,5 +1404,12 @@
       "cmd-enter up": "dev::Zeta2RatePredictionPositive",
       "cmd-enter down": "dev::Zeta2RatePredictionNegative"
     }
+  },
+  {
+    "context": "Zeta2Context > Editor",
+    "bindings": {
+      "alt-left": "dev::Zeta2ContextGoBack",
+      "alt-right": "dev::Zeta2ContextGoForward"
+    }
   }
 ]

assets/keymaps/default-windows.json 🔗

@@ -1327,5 +1327,12 @@
       "ctrl-enter up": "dev::Zeta2RatePredictionPositive",
       "ctrl-enter down": "dev::Zeta2RatePredictionNegative"
     }
+  },
+  {
+    "context": "Zeta2Context > Editor",
+    "bindings": {
+      "alt-left": "dev::Zeta2ContextGoBack",
+      "alt-right": "dev::Zeta2ContextGoForward"
+    }
   }
 ]

crates/zeta2/src/related_excerpts.rs 🔗

@@ -1,10 +1,13 @@
-use std::{cmp::Reverse, fmt::Write, ops::Range, path::PathBuf, sync::Arc};
+use std::{cmp::Reverse, fmt::Write, ops::Range, path::PathBuf, sync::Arc, time::Instant};
 
-use crate::merge_excerpts::write_merged_excerpts;
+use crate::{
+    ZetaContextRetrievalDebugInfo, ZetaDebugInfo, ZetaSearchQueryDebugInfo,
+    merge_excerpts::write_merged_excerpts,
+};
 use anyhow::{Result, anyhow};
 use collections::HashMap;
 use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions, Line};
-use futures::{StreamExt, stream::BoxStream};
+use futures::{StreamExt, channel::mpsc, stream::BoxStream};
 use gpui::{App, AsyncApp, Entity, Task};
 use indoc::indoc;
 use language::{Anchor, Bias, Buffer, OffsetRangeExt, Point, TextBufferSnapshot, ToPoint as _};
@@ -61,22 +64,22 @@ const SEARCH_TOOL_NAME: &str = "search";
 /// Search for relevant code
 ///
 /// For the best results, run multiple queries at once with a single invocation of this tool.
-#[derive(Deserialize, JsonSchema)]
-struct SearchToolInput {
+#[derive(Clone, Deserialize, JsonSchema)]
+pub struct SearchToolInput {
     /// An array of queries to run for gathering context relevant to the next prediction
     #[schemars(length(max = 5))]
-    queries: Box<[SearchToolQuery]>,
+    pub queries: Box<[SearchToolQuery]>,
 }
 
-#[derive(Deserialize, JsonSchema)]
-struct SearchToolQuery {
+#[derive(Debug, Clone, Deserialize, JsonSchema)]
+pub struct SearchToolQuery {
     /// A glob pattern to match file paths in the codebase
-    glob: String,
+    pub glob: String,
     /// A regular expression to match content within the files matched by the glob pattern
-    regex: String,
+    pub regex: String,
     /// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
     #[serde(default)]
-    case_sensitive: bool,
+    pub case_sensitive: bool,
 }
 
 const RESULTS_MESSAGE: &str = indoc! {"
@@ -124,6 +127,7 @@ pub fn find_related_excerpts<'a>(
     project: &Entity<Project>,
     events: impl Iterator<Item = &'a crate::Event>,
     options: &LlmContextOptions,
+    debug_tx: Option<mpsc::UnboundedSender<ZetaDebugInfo>>,
     cx: &App,
 ) -> Task<Result<HashMap<Entity<Buffer>, Vec<Range<Anchor>>>>> {
     let language_model_registry = LanguageModelRegistry::global(cx);
@@ -304,11 +308,33 @@ pub fn find_related_excerpts<'a>(
             snapshot: TextBufferSnapshot,
         }
 
-        let mut result_buffers_by_path = HashMap::default();
+        let search_queries = search_calls
+            .iter()
+            .map(|(_, tool_use)| {
+                Ok(serde_json::from_value::<SearchToolInput>(
+                    tool_use.input.clone(),
+                )?)
+            })
+            .collect::<Result<Vec<_>>>()?;
+
+        if let Some(debug_tx) = &debug_tx {
+            debug_tx
+                .unbounded_send(ZetaDebugInfo::SearchQueriesGenerated(
+                    ZetaSearchQueryDebugInfo {
+                        project: project.clone(),
+                        timestamp: Instant::now(),
+                        queries: search_queries
+                            .iter()
+                            .flat_map(|call| call.queries.iter().cloned())
+                            .collect(),
+                    },
+                ))
+                .ok();
+        }
 
-        for (index, tool_use) in search_calls.into_iter().rev() {
-            let call = serde_json::from_value::<SearchToolInput>(tool_use.input.clone())?;
+        let mut result_buffers_by_path = HashMap::default();
 
+        for ((index, tool_use), call) in search_calls.into_iter().zip(search_queries).rev() {
             let mut excerpts_by_buffer = HashMap::default();
 
             for query in call.queries {
@@ -392,6 +418,17 @@ pub fn find_related_excerpts<'a>(
                     },
                 ],
             );
+
+            if let Some(debug_tx) = &debug_tx {
+                debug_tx
+                    .unbounded_send(ZetaDebugInfo::SearchQueriesExecuted(
+                        ZetaContextRetrievalDebugInfo {
+                            project: project.clone(),
+                            timestamp: Instant::now(),
+                        },
+                    ))
+                    .ok();
+            }
         }
 
         if result_buffers_by_path.is_empty() {

crates/zeta2/src/zeta2.rs 🔗

@@ -45,8 +45,8 @@ mod related_excerpts;
 
 use crate::merge_excerpts::merge_excerpts;
 use crate::prediction::EditPrediction;
-pub use crate::related_excerpts::LlmContextOptions;
 use crate::related_excerpts::find_related_excerpts;
+pub use crate::related_excerpts::{LlmContextOptions, SearchToolQuery};
 pub use provider::ZetaEditPredictionProvider;
 
 const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1);
@@ -107,7 +107,7 @@ pub struct Zeta {
     projects: HashMap<EntityId, ZetaProject>,
     options: ZetaOptions,
     update_required: bool,
-    debug_tx: Option<mpsc::UnboundedSender<PredictionDebugInfo>>,
+    debug_tx: Option<mpsc::UnboundedSender<ZetaDebugInfo>>,
 }
 
 #[derive(Debug, Clone, PartialEq)]
@@ -134,7 +134,20 @@ impl ContextMode {
     }
 }
 
-pub struct PredictionDebugInfo {
+pub enum ZetaDebugInfo {
+    ContextRetrievalStarted(ZetaContextRetrievalDebugInfo),
+    SearchQueriesGenerated(ZetaSearchQueryDebugInfo),
+    SearchQueriesExecuted(ZetaContextRetrievalDebugInfo),
+    ContextRetrievalFinished(ZetaContextRetrievalDebugInfo),
+    EditPredicted(ZetaEditPredictionDebugInfo),
+}
+
+pub struct ZetaContextRetrievalDebugInfo {
+    pub project: Entity<Project>,
+    pub timestamp: Instant,
+}
+
+pub struct ZetaEditPredictionDebugInfo {
     pub request: predict_edits_v3::PredictEditsRequest,
     pub retrieval_time: TimeDelta,
     pub buffer: WeakEntity<Buffer>,
@@ -143,6 +156,12 @@ pub struct PredictionDebugInfo {
     pub response_rx: oneshot::Receiver<Result<predict_edits_v3::PredictEditsResponse, String>>,
 }
 
+pub struct ZetaSearchQueryDebugInfo {
+    pub project: Entity<Project>,
+    pub timestamp: Instant,
+    pub queries: Vec<SearchToolQuery>,
+}
+
 pub type RequestDebugInfo = predict_edits_v3::DebugInfo;
 
 struct ZetaProject {
@@ -303,7 +322,7 @@ impl Zeta {
         }
     }
 
-    pub fn debug_info(&mut self) -> mpsc::UnboundedReceiver<PredictionDebugInfo> {
+    pub fn debug_info(&mut self) -> mpsc::UnboundedReceiver<ZetaDebugInfo> {
         let (debug_watch_tx, debug_watch_rx) = mpsc::unbounded();
         self.debug_tx = Some(debug_watch_tx);
         debug_watch_rx
@@ -324,11 +343,30 @@ impl Zeta {
     }
 
     pub fn history_for_project(&self, project: &Entity<Project>) -> impl Iterator<Item = &Event> {
-        static EMPTY_EVENTS: VecDeque<Event> = VecDeque::new();
         self.projects
             .get(&project.entity_id())
-            .map_or(&EMPTY_EVENTS, |project| &project.events)
-            .iter()
+            .map(|project| project.events.iter())
+            .into_iter()
+            .flatten()
+    }
+
+    pub fn context_for_project(
+        &self,
+        project: &Entity<Project>,
+    ) -> impl Iterator<Item = (Entity<Buffer>, &[Range<Anchor>])> {
+        self.projects
+            .get(&project.entity_id())
+            .and_then(|project| {
+                Some(
+                    project
+                        .context
+                        .as_ref()?
+                        .iter()
+                        .map(|(buffer, ranges)| (buffer.clone(), ranges.as_slice())),
+                )
+            })
+            .into_iter()
+            .flatten()
     }
 
     pub fn usage(&self, cx: &App) -> Option<EditPredictionUsage> {
@@ -781,24 +819,19 @@ impl Zeta {
                 let debug_response_tx = if let Some(debug_tx) = &debug_tx {
                     let (response_tx, response_rx) = oneshot::channel();
 
-                    if !request.referenced_declarations.is_empty() || !request.signatures.is_empty()
-                    {
-                    } else {
-                    };
-
                     let local_prompt = build_prompt(&request)
                         .map(|(prompt, _)| prompt)
                         .map_err(|err| err.to_string());
 
                     debug_tx
-                        .unbounded_send(PredictionDebugInfo {
+                        .unbounded_send(ZetaDebugInfo::EditPredicted(ZetaEditPredictionDebugInfo {
                             request: request.clone(),
                             retrieval_time,
                             buffer: buffer.downgrade(),
                             local_prompt,
                             position,
                             response_rx,
-                        })
+                        }))
                         .ok();
                     Some(response_tx)
                 } else {
@@ -1047,7 +1080,20 @@ impl Zeta {
             return;
         };
 
+        let debug_tx = self.debug_tx.clone();
+
         zeta_project.refresh_context_task = Some(cx.spawn(async move |this, cx| {
+            if let Some(debug_tx) = &debug_tx {
+                debug_tx
+                    .unbounded_send(ZetaDebugInfo::ContextRetrievalStarted(
+                        ZetaContextRetrievalDebugInfo {
+                            project: project.clone(),
+                            timestamp: Instant::now(),
+                        },
+                    ))
+                    .ok();
+            }
+
             let related_excerpts = this
                 .update(cx, |this, cx| {
                     let Some(zeta_project) = this.projects.get(&project.entity_id()) else {
@@ -1064,6 +1110,7 @@ impl Zeta {
                         &project,
                         zeta_project.events.iter(),
                         options,
+                        debug_tx,
                         cx,
                     )
                 })
@@ -1077,6 +1124,16 @@ impl Zeta {
                 };
                 zeta_project.context = Some(related_excerpts);
                 zeta_project.refresh_context_task.take();
+                if let Some(debug_tx) = &this.debug_tx {
+                    debug_tx
+                        .unbounded_send(ZetaDebugInfo::ContextRetrievalFinished(
+                            ZetaContextRetrievalDebugInfo {
+                                project,
+                                timestamp: Instant::now(),
+                            },
+                        ))
+                        .ok();
+                }
             })
             .ok()
         }));

crates/zeta2_tools/Cargo.toml 🔗

@@ -12,6 +12,7 @@ workspace = true
 path = "src/zeta2_tools.rs"
 
 [dependencies]
+anyhow.workspace = true
 chrono.workspace = true
 client.workspace = true
 cloud_llm_client.workspace = true

crates/zeta2_tools/src/zeta2_context_view.rs 🔗

@@ -0,0 +1,412 @@
+use std::{
+    any::TypeId,
+    collections::VecDeque,
+    ops::Add,
+    sync::Arc,
+    time::{Duration, Instant},
+};
+
+use anyhow::Result;
+use client::{Client, UserStore};
+use editor::{Editor, PathKey};
+use futures::StreamExt as _;
+use gpui::{
+    Animation, AnimationExt, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle,
+    Focusable, ParentElement as _, SharedString, Styled as _, Task, TextAlign, Window, actions,
+    pulsating_between,
+};
+use multi_buffer::MultiBuffer;
+use project::Project;
+use text::OffsetRangeExt;
+use ui::{
+    ButtonCommon, Clickable, Color, Disableable, FluentBuilder as _, Icon, IconButton, IconName,
+    IconSize, InteractiveElement, IntoElement, ListItem, StyledTypography, div, h_flex, v_flex,
+};
+use workspace::{Item, ItemHandle as _};
+use zeta2::{
+    SearchToolQuery, Zeta, ZetaContextRetrievalDebugInfo, ZetaDebugInfo, ZetaSearchQueryDebugInfo,
+};
+
+pub struct Zeta2ContextView {
+    empty_focus_handle: FocusHandle,
+    project: Entity<Project>,
+    zeta: Entity<Zeta>,
+    runs: VecDeque<RetrievalRun>,
+    current_ix: usize,
+    _update_task: Task<Result<()>>,
+}
+
+#[derive(Debug)]
+pub struct RetrievalRun {
+    editor: Entity<Editor>,
+    search_queries: Vec<SearchToolQuery>,
+    started_at: Instant,
+    search_results_generated_at: Option<Instant>,
+    search_results_executed_at: Option<Instant>,
+    finished_at: Option<Instant>,
+}
+
+actions!(
+    dev,
+    [
+        /// Go to the previous context retrieval run
+        Zeta2ContextGoBack,
+        /// Go to the next context retrieval run
+        Zeta2ContextGoForward
+    ]
+);
+
+impl Zeta2ContextView {
+    pub fn new(
+        project: Entity<Project>,
+        client: &Arc<Client>,
+        user_store: &Entity<UserStore>,
+        window: &mut gpui::Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let zeta = Zeta::global(client, user_store, cx);
+
+        let mut debug_rx = zeta.update(cx, |zeta, _| zeta.debug_info());
+        let _update_task = cx.spawn_in(window, async move |this, cx| {
+            while let Some(event) = debug_rx.next().await {
+                this.update_in(cx, |this, window, cx| {
+                    this.handle_zeta_event(event, window, cx)
+                })?;
+            }
+            Ok(())
+        });
+
+        Self {
+            empty_focus_handle: cx.focus_handle(),
+            project,
+            runs: VecDeque::new(),
+            current_ix: 0,
+            zeta,
+            _update_task,
+        }
+    }
+
+    fn handle_zeta_event(
+        &mut self,
+        event: ZetaDebugInfo,
+        window: &mut gpui::Window,
+        cx: &mut Context<Self>,
+    ) {
+        match event {
+            ZetaDebugInfo::ContextRetrievalStarted(info) => {
+                if info.project == self.project {
+                    self.handle_context_retrieval_started(info, window, cx);
+                }
+            }
+            ZetaDebugInfo::SearchQueriesGenerated(info) => {
+                if info.project == self.project {
+                    self.handle_search_queries_generated(info, window, cx);
+                }
+            }
+            ZetaDebugInfo::SearchQueriesExecuted(info) => {
+                if info.project == self.project {
+                    self.handle_search_queries_executed(info, window, cx);
+                }
+            }
+            ZetaDebugInfo::ContextRetrievalFinished(info) => {
+                if info.project == self.project {
+                    self.handle_context_retrieval_finished(info, window, cx);
+                }
+            }
+            ZetaDebugInfo::EditPredicted(_) => {}
+        }
+    }
+
+    fn handle_context_retrieval_started(
+        &mut self,
+        info: ZetaContextRetrievalDebugInfo,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if self
+            .runs
+            .back()
+            .is_some_and(|run| run.search_results_executed_at.is_none())
+        {
+            self.runs.pop_back();
+        }
+
+        let multibuffer = cx.new(|_| MultiBuffer::new(language::Capability::ReadOnly));
+        let editor = cx
+            .new(|cx| Editor::for_multibuffer(multibuffer, Some(self.project.clone()), window, cx));
+
+        if self.runs.len() == 32 {
+            self.runs.pop_front();
+        }
+
+        self.runs.push_back(RetrievalRun {
+            editor,
+            search_queries: Vec::new(),
+            started_at: info.timestamp,
+            search_results_generated_at: None,
+            search_results_executed_at: None,
+            finished_at: None,
+        });
+
+        cx.notify();
+    }
+
+    fn handle_context_retrieval_finished(
+        &mut self,
+        info: ZetaContextRetrievalDebugInfo,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(run) = self.runs.back_mut() else {
+            return;
+        };
+
+        run.finished_at = Some(info.timestamp);
+
+        let multibuffer = run.editor.read(cx).buffer().clone();
+        multibuffer.update(cx, |multibuffer, cx| {
+            multibuffer.clear(cx);
+
+            let context = self.zeta.read(cx).context_for_project(&self.project);
+            let mut paths = Vec::new();
+            for (buffer, ranges) in context {
+                let path = PathKey::for_buffer(&buffer, cx);
+                let snapshot = buffer.read(cx).snapshot();
+                let ranges = ranges
+                    .iter()
+                    .map(|range| range.to_point(&snapshot))
+                    .collect::<Vec<_>>();
+                paths.push((path, buffer, ranges));
+            }
+
+            for (path, buffer, ranges) in paths {
+                multibuffer.set_excerpts_for_path(path, buffer, ranges, 0, cx);
+            }
+        });
+
+        run.editor.update(cx, |editor, cx| {
+            editor.move_to_beginning(&Default::default(), window, cx);
+        });
+
+        cx.notify();
+    }
+
+    fn handle_search_queries_generated(
+        &mut self,
+        info: ZetaSearchQueryDebugInfo,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(run) = self.runs.back_mut() else {
+            return;
+        };
+
+        run.search_results_generated_at = Some(info.timestamp);
+        run.search_queries = info.queries;
+        cx.notify();
+    }
+
+    fn handle_search_queries_executed(
+        &mut self,
+        info: ZetaContextRetrievalDebugInfo,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if self.current_ix + 2 == self.runs.len() {
+            // Switch to latest when the queries are executed
+            self.current_ix += 1;
+        }
+
+        let Some(run) = self.runs.back_mut() else {
+            return;
+        };
+
+        run.search_results_executed_at = Some(info.timestamp);
+        cx.notify();
+    }
+
+    fn handle_go_back(
+        &mut self,
+        _: &Zeta2ContextGoBack,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.current_ix = self.current_ix.saturating_sub(1);
+        cx.focus_self(window);
+        cx.notify();
+    }
+
+    fn handle_go_forward(
+        &mut self,
+        _: &Zeta2ContextGoForward,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.current_ix = self
+            .current_ix
+            .add(1)
+            .min(self.runs.len().saturating_sub(1));
+        cx.focus_self(window);
+        cx.notify();
+    }
+
+    fn render_informational_footer(&self, cx: &mut Context<'_, Zeta2ContextView>) -> ui::Div {
+        let is_latest = self.runs.len() == self.current_ix + 1;
+        let run = &self.runs[self.current_ix];
+
+        h_flex()
+            .w_full()
+            .font_buffer(cx)
+            .text_xs()
+            .border_t_1()
+            .child(
+                v_flex()
+                    .h_full()
+                    .flex_1()
+                    .children(run.search_queries.iter().enumerate().map(|(ix, query)| {
+                        ListItem::new(ix)
+                            .start_slot(
+                                Icon::new(IconName::MagnifyingGlass)
+                                    .color(Color::Muted)
+                                    .size(IconSize::Small),
+                            )
+                            .child(query.regex.clone())
+                    })),
+            )
+            .child(
+                v_flex()
+                    .h_full()
+                    .pr_2()
+                    .text_align(TextAlign::Right)
+                    .child(
+                        h_flex()
+                            .justify_end()
+                            .child(
+                                IconButton::new("go-back", IconName::ChevronLeft)
+                                    .disabled(self.current_ix == 0 || self.runs.len() < 2)
+                                    .tooltip(ui::Tooltip::for_action_title(
+                                        "Go to previous run",
+                                        &Zeta2ContextGoBack,
+                                    ))
+                                    .on_click(cx.listener(|this, _, window, cx| {
+                                        this.handle_go_back(&Zeta2ContextGoBack, window, cx);
+                                    })),
+                            )
+                            .child(
+                                div()
+                                    .child(format!("{}/{}", self.current_ix + 1, self.runs.len()))
+                                    .map(|this| {
+                                        if self.runs.back().is_some_and(|back| {
+                                            back.search_results_executed_at.is_none()
+                                        }) {
+                                            this.with_animation(
+                                                "pulsating-count",
+                                                Animation::new(Duration::from_secs(2))
+                                                    .repeat()
+                                                    .with_easing(pulsating_between(0.4, 0.8)),
+                                                |label, delta| label.opacity(delta),
+                                            )
+                                            .into_any_element()
+                                        } else {
+                                            this.into_any_element()
+                                        }
+                                    }),
+                            )
+                            .child(
+                                IconButton::new("go-forward", IconName::ChevronRight)
+                                    .disabled(self.current_ix + 1 == self.runs.len())
+                                    .tooltip(ui::Tooltip::for_action_title(
+                                        "Go to next run",
+                                        &Zeta2ContextGoBack,
+                                    ))
+                                    .on_click(cx.listener(|this, _, window, cx| {
+                                        this.handle_go_forward(&Zeta2ContextGoForward, window, cx);
+                                    })),
+                            ),
+                    )
+                    .map(|mut div| {
+                        let t0 = run.started_at;
+                        let Some(t1) = run.search_results_generated_at else {
+                            return div.child("Planning search...");
+                        };
+                        div = div.child(format!("Planned search: {:>5} ms", (t1 - t0).as_millis()));
+
+                        let Some(t2) = run.search_results_executed_at else {
+                            return div.child("Running search...");
+                        };
+                        div = div.child(format!("Ran search: {:>5} ms", (t2 - t1).as_millis()));
+
+                        let Some(t3) = run.finished_at else {
+                            if is_latest {
+                                return div.child("Filtering results...");
+                            } else {
+                                return div.child("Canceled");
+                            }
+                        };
+                        div.child(format!("Filtered results: {:>5} ms", (t3 - t2).as_millis()))
+                    }),
+            )
+    }
+}
+
+impl Focusable for Zeta2ContextView {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        self.runs
+            .get(self.current_ix)
+            .map(|run| run.editor.read(cx).focus_handle(cx))
+            .unwrap_or_else(|| self.empty_focus_handle.clone())
+    }
+}
+
+impl EventEmitter<()> for Zeta2ContextView {}
+
+impl Item for Zeta2ContextView {
+    type Event = ();
+
+    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+        "Edit Prediction Context".into()
+    }
+
+    fn buffer_kind(&self, _cx: &App) -> workspace::item::ItemBufferKind {
+        workspace::item::ItemBufferKind::Multibuffer
+    }
+
+    fn act_as_type<'a>(
+        &'a self,
+        type_id: TypeId,
+        self_handle: &'a Entity<Self>,
+        _: &'a App,
+    ) -> Option<gpui::AnyView> {
+        if type_id == TypeId::of::<Self>() {
+            Some(self_handle.to_any())
+        } else if type_id == TypeId::of::<Editor>() {
+            Some(self.runs.get(self.current_ix)?.editor.to_any())
+        } else {
+            None
+        }
+    }
+}
+
+impl gpui::Render for Zeta2ContextView {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
+        v_flex()
+            .key_context("Zeta2Context")
+            .on_action(cx.listener(Self::handle_go_back))
+            .on_action(cx.listener(Self::handle_go_forward))
+            .size_full()
+            .map(|this| {
+                if self.runs.is_empty() {
+                    this.child(
+                        v_flex()
+                            .size_full()
+                            .justify_center()
+                            .items_center()
+                            .child("No retrieval runs yet"),
+                    )
+                } else {
+                    this.child(self.runs[self.current_ix].editor.clone())
+                        .child(self.render_informational_footer(cx))
+                }
+            })
+    }
+}

crates/zeta2_tools/src/zeta2_tools.rs 🔗

@@ -1,3 +1,5 @@
+mod zeta2_context_view;
+
 use std::{cmp::Reverse, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
 
 use chrono::TimeDelta;
@@ -21,16 +23,19 @@ use ui_input::InputField;
 use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
 use workspace::{Item, SplitDirection, Workspace};
 use zeta2::{
-    ContextMode, DEFAULT_SYNTAX_CONTEXT_OPTIONS, LlmContextOptions, PredictionDebugInfo, Zeta,
-    Zeta2FeatureFlag, ZetaOptions,
+    ContextMode, DEFAULT_SYNTAX_CONTEXT_OPTIONS, LlmContextOptions, Zeta, Zeta2FeatureFlag,
+    ZetaDebugInfo, ZetaEditPredictionDebugInfo, ZetaOptions,
 };
 
 use edit_prediction_context::{EditPredictionContextOptions, EditPredictionExcerptOptions};
+use zeta2_context_view::Zeta2ContextView;
 
 actions!(
     dev,
     [
-        /// Opens the language server protocol logs viewer.
+        /// Opens the edit prediction context view.
+        OpenZeta2ContextView,
+        /// Opens the edit prediction inspector.
         OpenZeta2Inspector,
         /// Rate prediction as positive.
         Zeta2RatePredictionPositive,
@@ -60,6 +65,27 @@ pub fn init(cx: &mut App) {
         });
     })
     .detach();
+
+    cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
+        workspace.register_action(move |workspace, _: &OpenZeta2ContextView, window, cx| {
+            let project = workspace.project();
+            workspace.split_item(
+                SplitDirection::Right,
+                Box::new(cx.new(|cx| {
+                    Zeta2ContextView::new(
+                        project.clone(),
+                        workspace.client(),
+                        workspace.user_store(),
+                        window,
+                        cx,
+                    )
+                })),
+                window,
+                cx,
+            );
+        });
+    })
+    .detach();
 }
 
 // TODO show included diagnostics, and events
@@ -320,7 +346,7 @@ impl Zeta2Inspector {
 
     fn update_last_prediction(
         &mut self,
-        prediction: zeta2::PredictionDebugInfo,
+        prediction: zeta2::ZetaDebugInfo,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -340,6 +366,9 @@ impl Zeta2Inspector {
             let language_registry = self.project.read(cx).languages().clone();
             async move |this, cx| {
                 let mut languages = HashMap::default();
+                let ZetaDebugInfo::EditPredicted(prediction) = prediction else {
+                    return;
+                };
                 for ext in prediction
                     .request
                     .referenced_declarations
@@ -450,7 +479,7 @@ impl Zeta2Inspector {
                         editor
                     });
 
-                    let PredictionDebugInfo {
+                    let ZetaEditPredictionDebugInfo {
                         response_rx,
                         position,
                         buffer,

docs/src/configuring-zed.md 🔗

@@ -9,7 +9,7 @@ In addition to the settings described here, you may also want to change your [th
 You can browse through many of the supported settings via the Settings Editor, which can be opened with the {#kb zed::OpenSettings} keybinding, or through the `zed: open settings` action in the command palette. Through it, you can customize your local, user settings as well as project settings.
 
 > Note that not all settings that Zed supports are available through the Settings Editor yet.
-> Some more intricate ones, such as language formatters, can only be changed through the JSON settings file {#kb: zed::OpenSettingsFile}.
+> Some more intricate ones, such as language formatters, can only be changed through the JSON settings file {#kb zed::OpenSettingsFile}.
 
 ## User Settings File
 

docs/src/installation.md 🔗

@@ -59,7 +59,7 @@ Zed supports the follow macOS releases:
 
 The macOS releases labelled "Partially Supported" (Big Sur and Catalina) do not support screen sharing via Zed Collaboration. These features use the [LiveKit SDK](https://livekit.io) which relies upon [ScreenCaptureKit.framework](https://developer.apple.com/documentation/screencapturekit/) only available on macOS 12 (Monterey) and newer.
 
-**Mac Hardware**
+#### Mac Hardware
 
 Zed supports machines with Intel (x86_64) or Apple (aarch64) processors that meet the above macOS requirements:
 
@@ -90,7 +90,7 @@ Zed supports the follow Windows releases:
 | Windows 11 (all releases) | Supported | Supported |
 | Windows 10 (64-bit) | Supported | Supported |
 
-**Windows Hardware**
+#### Windows Hardware
 
 Zed supports machines with Intel or AMD 64-bit (x86_64) processors that meet the above Windows requirements:
 

script/bundle-mac 🔗

@@ -9,7 +9,6 @@ open_result=false
 local_arch=false
 local_only=false
 local_install=false
-bundle_name=""
 can_code_sign=false
 
 # This must match the team in the provisioning profile.
@@ -19,12 +18,11 @@ APPLE_NOTARIZATION_TEAM="MQ55VZLNZQ"
 # Function for displaying help info
 help_info() {
   echo "
-Usage: ${0##*/} [options] [bundle_name]
+Usage: ${0##*/} [options] [architecture=host]
 Build the application bundle for macOS.
 
 Options:
   -d    Compile in debug mode
-  -l    Compile for local architecture only.
   -o    Open dir with the resulting DMG or launch the app itself in local mode.
   -i    Install the resulting DMG into /Applications in local mode. Noop without -l.
   -h    Display this help and exit.
@@ -41,12 +39,6 @@ do
             build_flag="";
             target_dir="debug"
             ;;
-        l)
-            export CARGO_INCREMENTAL=true
-            export CARGO_BUNDLE_SKIP_BUILD=true
-            local_arch=true
-            local_only=true
-            ;;
         i) local_install=true;;
         h)
            help_info
@@ -57,11 +49,6 @@ done
 
 shift $((OPTIND-1))
 
-if [[ $# -gt 0 ]]; then
-    if [ "$1" ]; then
-        bundle_name=$1
-    fi
-fi
 
 # Get release channel
 pushd crates/zed
@@ -81,24 +68,31 @@ export CXXFLAGS="-stdlib=libc++"
 
 version_info=$(rustc --version --verbose)
 host_line=$(echo "$version_info" | grep host)
-local_target_triple=${host_line#*: }
+target_triple=${host_line#*: }
+if [[ $# -gt 0 && -n "$1" ]]; then
+    target_triple="$1"
+fi
+remote_server_arch=""
+
+if [[ "$target_triple" = "x86_64-apple-darwin" ]]; then
+    remote_server_arch="x86_64"
+elif [[ "$target_triple" = "aarch64-apple-darwin" ]]; then
+    remote_server_arch="aarch64"
+else
+    echo "Unsupported architecture $target_triple"
+    exit 1
+fi
 
 # Generate the licenses first, so they can be baked into the binaries
 script/generate-licenses
 
-if [ "$local_arch" = true ]; then
-    echo "Building for local target only."
-    cargo build ${build_flag} --package zed --package cli --package remote_server
-else
-    rustup target add aarch64-apple-darwin
-    rustup target add x86_64-apple-darwin
-
-    echo "Compiling zed binaries"
-    cargo build ${build_flag} --package zed --package cli --target aarch64-apple-darwin --target x86_64-apple-darwin
-    # Build remote_server in separate invocation to prevent feature unification from other crates
-    # from influencing dynamic libraries required by it.
-    cargo build ${build_flag} --package remote_server     --target aarch64-apple-darwin --target x86_64-apple-darwin
-fi
+rustup target add $target_triple
+
+echo "Compiling zed binaries"
+cargo build ${build_flag} --package zed --package cli --target $target_triple
+# Build remote_server in separate invocation to prevent feature unification from other crates
+# from influencing dynamic libraries required by it.
+cargo build ${build_flag} --package remote_server --target $target_triple
 
 echo "Creating application bundle"
 pushd crates/zed
@@ -108,13 +102,7 @@ sed \
     "s/package.metadata.bundle-${channel}/package.metadata.bundle/" \
     Cargo.toml
 
-if [ "$local_arch" = true ]; then
-    app_path=$(cargo bundle ${build_flag} --select-workspace-root | xargs)
-else
-    app_path_x64=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs)
-    app_path_aarch64=$(cargo bundle ${build_flag} --target aarch64-apple-darwin --select-workspace-root | xargs)
-    app_path=$app_path_x64
-fi
+app_path=$(cargo bundle ${build_flag} --target $target_triple --select-workspace-root | xargs)
 
 mv Cargo.toml.backup Cargo.toml
 popd
@@ -189,26 +177,12 @@ function download_git() {
     rm -rf "$tmp_dir"
 }
 
-function prepare_binaries() {
-    local architecture=$1
-    local app_path=$2
-
-    cp target/${architecture}/${target_dir}/zed "${app_path}/Contents/MacOS/zed"
-    cp target/${architecture}/${target_dir}/cli "${app_path}/Contents/MacOS/cli"
-}
-
 function sign_app_binaries() {
-    local app_path=$1
-    local architecture=$2
-    local architecture_dir=$3
     rm -rf "${app_path}/Contents/Frameworks"
     mkdir -p "${app_path}/Contents/Frameworks"
-    if [ "$local_arch" = true ]; then
-        cp -R target/${target_dir}/cli "${app_path}/Contents/MacOS/"
-    fi
 
     echo "Downloading git binary"
-    download_git "${architecture}" "${app_path}/Contents/MacOS/git"
+    download_git "${target_triple}" "${app_path}/Contents/MacOS/git"
 
     # Note: The app identifier for our development builds is the same as the app identifier for nightly.
     cp crates/zed/contents/$channel/embedded.provisionprofile "${app_path}/Contents/"
@@ -251,15 +225,7 @@ function sign_app_binaries() {
         exit 0
     fi
 
-    # If bundle_name is not set or empty, use the basename of $app_path
-    if [ -z "$bundle_name" ]; then
-        bundle_name=$(basename "$app_path")
-    else
-        # If bundle_name doesn't end in .app, append it
-        if [[ "$bundle_name" != *.app ]]; then
-            bundle_name="$bundle_name.app"
-        fi
-    fi
+    bundle_name=$(basename "$app_path")
 
     if [ "$local_only" = true ]; then
         if [ "$local_install" = true ]; then
@@ -277,7 +243,7 @@ function sign_app_binaries() {
             fi
         fi
     else
-        dmg_target_directory="target/${architecture_dir}/${target_dir}"
+        dmg_target_directory="target/${target_triple}/${target_dir}"
         dmg_source_directory="${dmg_target_directory}/dmg"
         dmg_file_path="${dmg_target_directory}/Zed.dmg"
         xcode_bin_dir_path="$(xcode-select -p)/usr/bin"
@@ -325,44 +291,29 @@ function sign_binary() {
         /usr/bin/codesign --deep --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "$IDENTITY" "${binary_path}" -v
     fi
 }
+cp target/${target_triple}/${target_dir}/zed "${app_path}/Contents/MacOS/zed"
+cp target/${target_triple}/${target_dir}/cli "${app_path}/Contents/MacOS/cli"
+sign_app_binaries
 
-if [ "$local_arch" = true ]; then
-    sign_app_binaries "$app_path" "$local_target_triple" "$local_target_triple"
-
-    sign_binary "target/release/remote_server"
-else
-    # Create universal binary
-    prepare_binaries "aarch64-apple-darwin" "$app_path_aarch64"
-    prepare_binaries "x86_64-apple-darwin" "$app_path_x64"
-
-
-    sign_app_binaries "$app_path_x64" "x86_64-apple-darwin" "x86_64-apple-darwin"
-    sign_app_binaries "$app_path_aarch64" "aarch64-apple-darwin" "aarch64-apple-darwin"
-
-    sign_binary "target/x86_64-apple-darwin/release/remote_server"
-    sign_binary "target/aarch64-apple-darwin/release/remote_server"
-    gzip -f --stdout --best target/x86_64-apple-darwin/release/remote_server > target/zed-remote-server-macos-x86_64.gz
-    gzip -f --stdout --best target/aarch64-apple-darwin/release/remote_server > target/zed-remote-server-macos-aarch64.gz
-fi
+sign_binary "target/$target_triple/release/remote_server"
+gzip -f --stdout --best target/$target_triple/release/remote_server > target/zed-remote-server-macos-$remote_server_arch.gz
 
 function upload_debug_info() {
-    architecture=$1
     if [[ -n "${SENTRY_AUTH_TOKEN:-}" ]]; then
         echo "Uploading zed debug symbols to sentry..."
         # note: this uploads the unstripped binary which is needed because it contains
         # .eh_frame data for stack unwinding. see https://github.com/getsentry/symbolic/issues/783
         sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev \
-            "target/${architecture}/${target_dir}/zed" \
-            "target/${architecture}/${target_dir}/remote_server" \
-            "target/${architecture}/${target_dir}/zed.dwarf"
+            "target/${target_triple}/${target_dir}/zed" \
+            "target/${target_triple}/${target_dir}/remote_server" \
+            "target/${target_triple}/${target_dir}/zed.dwarf"
     else
         echo "missing SENTRY_AUTH_TOKEN. skipping sentry upload."
     fi
 }
 
 if command -v sentry-cli >/dev/null 2>&1; then
-    upload_debug_info "aarch64-apple-darwin"
-    upload_debug_info "x86_64-apple-darwin"
+    upload_debug_info
 else
     echo "sentry-cli not found. skipping sentry upload."
     echo "install with: 'curl -sL https://sentry.io/get-cli | bash'"

script/upload-nightly 🔗

@@ -14,20 +14,31 @@ is_allowed_target() {
     return 1
 }
 
-if [[ -n "${1:-}" ]]; then
-    if is_allowed_target "$1"; then
-        target="$1"
-    else
-        echo "Error: Target '$1' is not allowed"
-        echo "Usage: $0 [${allowed_targets[*]}]"
-        exit 1
-    fi
+allowed_arch=("x86_64" "aarch64")
+is_allowed_arch() {
+    for val in "${allowed_arch[@]}"; do
+        if [[ "$1" == "$val" ]]; then
+            return 0
+        fi
+    done
+    return 1
+}
+
+if is_allowed_target "$1"; then
+    target="$1"
+else
+    echo "Error: Target '$1' is not allowed"
+    echo "Usage: $0 [${allowed_targets[*]}] {arch}"
+    exit 1
+fi
+if is_allowed_arch "$2"; then
+    arch="$2"
 else
-echo "Error: Target is not specified"
-echo "Usage: $0 [${allowed_targets[*]}]"
-exit 1
+    echo "Error: Arch '$2' is not allowed"
+    echo "Usage: $0 $1 [${allowed_arch[*]}]"
+    exit 1
 fi
-echo "Uploading nightly for target: $target"
+echo "Uploading nightly for target: $target $arch"
 
 bucket_name="zed-nightly-host"
 
@@ -41,10 +52,9 @@ done
 
 case "$target" in
     macos)
-        upload_to_blob_store $bucket_name "target/aarch64-apple-darwin/release/Zed.dmg" "nightly/Zed-aarch64.dmg"
-        upload_to_blob_store $bucket_name "target/x86_64-apple-darwin/release/Zed.dmg" "nightly/Zed-x86_64.dmg"
+        upload_to_blob_store $bucket_name "target/$arch-apple-darwin/release/Zed.dmg" "nightly/Zed-$arch.dmg"
         upload_to_blob_store $bucket_name "target/latest-sha" "nightly/latest-sha"
-        rm -f "target/aarch64-apple-darwin/release/Zed.dmg" "target/x86_64-apple-darwin/release/Zed.dmg" "target/release/Zed.dmg"
+        rm -f "target/$arch-apple-darwin/release/Zed.dmg" "target/release/Zed.dmg"
         rm -f "target/latest-sha"
         ;;
     linux-targz)

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

@@ -1,5 +1,6 @@
 use crate::tasks::workflows::{
     nix_build::build_nix,
+    run_bundling::bundle_mac,
     runners::{Arch, Platform},
     steps::NamedJob,
     vars::{mac_bundle_envs, windows_bundle_envs},
@@ -33,7 +34,8 @@ pub fn release_nightly() -> Workflow {
     let style = check_style();
     let tests = run_tests(Platform::Mac);
     let windows_tests = run_tests(Platform::Windows);
-    let bundle_mac = bundle_mac_nightly(&[&style, &tests]);
+    let bundle_mac_x86 = bundle_mac_nightly(Arch::X86_64, &[&style, &tests]);
+    let bundle_mac_arm = bundle_mac_nightly(Arch::ARM64, &[&style, &tests]);
     let linux_x86 = bundle_linux_nightly(Arch::X86_64, &[&style, &tests]);
     let linux_arm = bundle_linux_nightly(Arch::ARM64, &[&style, &tests]);
     let windows_x86 = bundle_windows_nightly(Arch::X86_64, &[&style, &windows_tests]);
@@ -54,7 +56,8 @@ pub fn release_nightly() -> Workflow {
         &[&style, &tests],
     );
     let update_nightly_tag = update_nightly_tag_job(&[
-        &bundle_mac,
+        &bundle_mac_x86,
+        &bundle_mac_arm,
         &linux_x86,
         &linux_arm,
         &windows_x86,
@@ -70,7 +73,8 @@ pub fn release_nightly() -> Workflow {
         .add_job(style.name, style.job)
         .add_job(tests.name, tests.job)
         .add_job(windows_tests.name, windows_tests.job)
-        .add_job(bundle_mac.name, bundle_mac.job)
+        .add_job(bundle_mac_x86.name, bundle_mac_x86.job)
+        .add_job(bundle_mac_arm.name, bundle_mac_arm.job)
         .add_job(linux_x86.name, linux_x86.job)
         .add_job(linux_arm.name, linux_arm.job)
         .add_job(windows_x86.name, windows_x86.job)
@@ -127,19 +131,21 @@ fn run_tests(platform: Platform) -> NamedJob {
     }
 }
 
-fn bundle_mac_nightly(deps: &[&NamedJob]) -> NamedJob {
+fn bundle_mac_nightly(arch: Arch, deps: &[&NamedJob]) -> NamedJob {
     let platform = Platform::Mac;
-    let job = release_job(deps)
-        .runs_on(runners::MAC_DEFAULT)
-        .envs(mac_bundle_envs())
-        .add_step(steps::checkout_repo())
-        .add_step(steps::setup_node())
-        .add_step(steps::setup_sentry())
-        .add_step(steps::clear_target_dir_if_large(platform))
-        .add_step(set_release_channel_to_nightly(platform))
-        .add_step(steps::script("./script/bundle-mac"))
-        .add_step(upload_zed_nightly(platform, Arch::ARM64));
-    named::job(job)
+    NamedJob {
+        name: format!("bundle_mac_nightly_{arch}"),
+        job: release_job(deps)
+            .runs_on(runners::MAC_DEFAULT)
+            .envs(mac_bundle_envs())
+            .add_step(steps::checkout_repo())
+            .add_step(steps::setup_node())
+            .add_step(steps::setup_sentry())
+            .add_step(steps::clear_target_dir_if_large(platform))
+            .add_step(set_release_channel_to_nightly(platform))
+            .add_step(bundle_mac(arch))
+            .add_step(upload_zed_nightly(platform, arch)),
+    }
 }
 
 fn bundle_linux_nightly(arch: Arch, deps: &[&NamedJob]) -> NamedJob {
@@ -216,8 +222,8 @@ fn add_rust_to_path() -> Step<Run> {
 
 fn upload_zed_nightly(platform: Platform, arch: Arch) -> Step<Run> {
     match platform {
-        Platform::Linux => named::bash("script/upload-nightly linux-targz"),
-        Platform::Mac => named::bash("script/upload-nightly macos"),
+        Platform::Linux => named::bash(&format!("script/upload-nightly linux-targz {arch}")),
+        Platform::Mac => named::bash(&format!("script/upload-nightly macos {arch}")),
         Platform::Windows => {
             let cmd = match arch {
                 Arch::X86_64 => "script/upload-nightly.ps1 -Architecture x86_64",

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

@@ -22,7 +22,8 @@ pub fn run_bundling() -> Workflow {
         .add_env(("RUST_BACKTRACE", "1"))
         .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED))
         .add_env(("ZED_MINIDUMP_ENDPOINT", vars::ZED_SENTRY_MINIDUMP_ENDPOINT))
-        .add_job("bundle_mac", bundle_mac())
+        .add_job("bundle_mac_x86_64", bundle_mac_job(runners::Arch::X86_64))
+        .add_job("bundle_mac_arm64", bundle_mac_job(runners::Arch::ARM64))
         .add_job("bundle_linux_x86_64", bundle_linux(runners::Arch::X86_64))
         .add_job("bundle_linux_arm64", bundle_linux(runners::Arch::ARM64))
         .add_job(
@@ -44,7 +45,8 @@ fn bundle_job() -> Job {
         .timeout_minutes(60u32)
 }
 
-fn bundle_mac() -> Job {
+fn bundle_mac_job(arch: runners::Arch) -> Job {
+    use vars::GITHUB_SHA;
     bundle_job()
         .runs_on(runners::MAC_DEFAULT)
         .envs(mac_bundle_envs())
@@ -52,17 +54,21 @@ fn bundle_mac() -> Job {
         .add_step(steps::setup_node())
         .add_step(steps::setup_sentry())
         .add_step(steps::clear_target_dir_if_large(runners::Platform::Mac))
-        .add_step(steps::script("./script/bundle-mac"))
+        .add_step(bundle_mac(arch))
         .add_step(steps::upload_artifact(
-            "Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg",
-            "target/aarch64-apple-darwin/release/Zed.dmg",
+            &format!("Zed_{GITHUB_SHA}-{arch}.dmg"),
+            &format!("target/{arch}-apple-darwin/release/Zed.dmg"),
         ))
         .add_step(steps::upload_artifact(
-            "Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg",
-            "target/x86_64-apple-darwin/release/Zed.dmg",
+            &format!("zed-remote-server-{GITHUB_SHA}-macos-{arch}.gz"),
+            &format!("target/zed-remote-server-macos-{arch}.gz"),
         ))
 }
 
+pub fn bundle_mac(arch: runners::Arch) -> Step<Run> {
+    named::bash(&format!("./script/bundle-mac {arch}-apple-darwin"))
+}
+
 fn bundle_linux(arch: runners::Arch) -> Job {
     let artifact_name = format!("zed-{}-{}.tar.gz", vars::GITHUB_SHA, arch.triple());
     let remote_server_artifact_name = format!(