Merge branch 'main' into agent-drawer

Max Brunsfeld created

Change summary

.cargo/ci-config.toml                                             |  10 
.cargo/config.toml                                                |   4 
.github/workflows/autofix_pr.yml                                  |   2 
.github/workflows/compare_perf.yml                                |   2 
.github/workflows/deploy_collab.yml                               |   4 
.github/workflows/extension_bump.yml                              |  38 
.github/workflows/pr_labeler.yml                                  |  53 
.github/workflows/release.yml                                     |   8 
.github/workflows/release_nightly.yml                             |   4 
.github/workflows/run_agent_evals.yml                             |   2 
.github/workflows/run_bundling.yml                                |   4 
.github/workflows/run_cron_unit_evals.yml                         |   2 
.github/workflows/run_tests.yml                                   |  10 
.github/workflows/run_unit_evals.yml                              |   2 
Cargo.lock                                                        |  23 
Cargo.toml                                                        |   5 
assets/icons/eye_off.svg                                          |   6 
assets/keymaps/default-linux.json                                 |   7 
assets/keymaps/default-macos.json                                 |   7 
assets/keymaps/default-windows.json                               |   7 
assets/settings/default.json                                      |  12 
assets/themes/ayu/ayu.json                                        |  36 
crates/acp_thread/src/acp_thread.rs                               | 178 
crates/agent/src/thread.rs                                        |   8 
crates/agent_servers/src/acp.rs                                   | 194 
crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs |  19 
crates/agent_ui/src/agent_panel.rs                                |  59 
crates/agent_ui/src/connection_view.rs                            |  10 
crates/agent_ui/src/connection_view/thread_view.rs                | 173 
crates/agent_ui/src/threads_archive_view.rs                       |  89 
crates/agent_ui/src/threads_panel.rs                              | 162 
crates/buffer_diff/src/buffer_diff.rs                             |  55 
crates/edit_prediction/src/edit_prediction.rs                     |  25 
crates/edit_prediction/src/edit_prediction_tests.rs               | 171 
crates/edit_prediction/src/zeta.rs                                |  40 
crates/edit_prediction_cli/src/format_prompt.rs                   |   2 
crates/editor/src/edit_prediction_tests.rs                        |  66 
crates/editor/src/editor.rs                                       |  24 
crates/editor/src/editor_tests.rs                                 |  94 
crates/file_finder/src/file_finder.rs                             |  58 
crates/file_finder/src/file_finder_tests.rs                       | 113 
crates/fs/Cargo.toml                                              |   1 
crates/fs/src/fs.rs                                               | 111 
crates/git_ui/Cargo.toml                                          |   1 
crates/git_ui/src/git_panel.rs                                    |  81 
crates/git_ui/src/git_panel_settings.rs                           |   4 
crates/gpui/src/text_system/line_wrapper.rs                       |   7 
crates/gpui_macos/src/window.rs                                   |   5 
crates/http_client/src/github_download.rs                         |   1 
crates/icons/src/icons.rs                                         |   1 
crates/language_models/Cargo.toml                                 |   1 
crates/language_models/src/provider/cloud.rs                      |  47 
crates/languages/src/cpp.rs                                       |  12 
crates/languages/src/cpp/semantic_token_rules.json                |   7 
crates/languages/src/lib.rs                                       |   1 
crates/languages/src/markdown/highlights.scm                      |   5 
crates/livekit_client/src/livekit_client/playback.rs              |  36 
crates/markdown_preview/Cargo.toml                                |   1 
crates/markdown_preview/src/markdown_parser.rs                    |   3 
crates/markdown_preview/src/markdown_preview_view.rs              |   1 
crates/outline/src/outline.rs                                     | 387 
crates/platform_title_bar/src/platform_title_bar.rs               |  10 
crates/project/src/lsp_command.rs                                 |   9 
crates/project/src/lsp_store.rs                                   |  32 
crates/proto/proto/lsp.proto                                      |   1 
crates/rules_library/src/rules_library.rs                         |  74 
crates/settings_content/src/agent.rs                              |  13 
crates/settings_content/src/settings_content.rs                   |  11 
crates/settings_ui/Cargo.toml                                     |   1 
crates/settings_ui/src/page_data.rs                               |  65 
crates/settings_ui/src/settings_ui.rs                             |   2 
crates/ui/src/components/ai/thread_item.rs                        |  39 
crates/ui_input/src/input_field.rs                                |  44 
crates/util/Cargo.toml                                            |   2 
crates/vim/src/helix.rs                                           |  33 
crates/vim/src/normal.rs                                          |   7 
crates/vim/src/normal/paste.rs                                    |  16 
crates/vim/src/normal/repeat.rs                                   | 219 
crates/vim/src/state.rs                                           |  10 
crates/vim/src/vim.rs                                             |  16 
crates/vim/test_data/test_dot_repeat_registers.json               | 125 
crates/vim/test_data/test_dot_repeat_registers_paste.json         | 105 
crates/which_key/src/which_key.rs                                 |   4 
crates/zed/src/zed.rs                                             |   2 
crates/zeta_prompt/src/zeta_prompt.rs                             | 180 
docs/src/SUMMARY.md                                               |   1 
docs/src/ai/external-agents.md                                    |   2 
docs/src/ai/plans-and-usage.md                                    |  10 
docs/src/ai/tools.md                                              |  16 
docs/src/roles.md                                                 |  71 
extensions/glsl/Cargo.toml                                        |   2 
extensions/glsl/extension.toml                                    |   2 
extensions/glsl/languages/glsl/highlights.scm                     | 170 
extensions/html/Cargo.toml                                        |   2 
extensions/html/extension.toml                                    |   2 
extensions/html/src/html.rs                                       |   7 
script/bundle-linux                                               |  20 
script/linux                                                      |   2 
tooling/xtask/src/tasks/workflows/extension_bump.rs               |  71 
tooling/xtask/src/tasks/workflows/steps.rs                        |   8 
100 files changed, 2,994 insertions(+), 913 deletions(-)

Detailed changes

.cargo/ci-config.toml πŸ”—

@@ -15,14 +15,4 @@ rustflags = ["-D", "warnings"]
 [profile.dev]
 debug = "limited"
 
-# Use Mold on Linux, because it's faster than GNU ld and LLD.
-#
-# We no longer set this in the default `config.toml` so that developers can opt in to Wild, which
-# is faster than Mold, in their own ~/.cargo/config.toml.
-[target.x86_64-unknown-linux-gnu]
-linker = "clang"
-rustflags = ["-C", "link-arg=-fuse-ld=mold"]
 
-[target.aarch64-unknown-linux-gnu]
-linker = "clang"
-rustflags = ["-C", "link-arg=-fuse-ld=mold"]

.cargo/config.toml πŸ”—

@@ -16,5 +16,9 @@ rustflags = [
     "target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
 ]
 
+# We need lld to link libwebrtc.a successfully on aarch64-linux
+[target.aarch64-unknown-linux-gnu]
+rustflags = ["-C", "link-arg=-fuse-ld=lld"]
+
 [env]
 MACOSX_DEPLOYMENT_TARGET = "10.15.7"

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

@@ -37,8 +37,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::setup_pnpm

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

@@ -30,8 +30,6 @@ jobs:
         cp ./.cargo/ci-config.toml ./../.cargo/config.toml
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: compare_perf::run_perf::install_hyperfine

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

@@ -32,8 +32,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::cargo_fmt
@@ -65,8 +63,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::cargo_install_nextest

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

@@ -83,6 +83,11 @@ jobs:
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
       with:
         clean: false
+    - name: steps::cache_rust_dependencies_namespace
+      uses: namespacelabs/nscloud-cache-action@v1
+      with:
+        cache: rust
+        path: ~/.rustup
     - name: extension_bump::install_bump_2_version
       run: pip install bump2version --break-system-packages
     - id: bump-version
@@ -116,7 +121,13 @@ jobs:
         else
             {
                 echo "title=${EXTENSION_ID}: Bump to v${NEW_VERSION}";
-                echo "body=This PR bumps the version of the ${EXTENSION_NAME} extension to v${NEW_VERSION}";
+                echo "body<<EOF";
+                echo "This PR bumps the version of the ${EXTENSION_NAME} extension to v${NEW_VERSION}.";
+                echo "";
+                echo "Release Notes:";
+                echo "";
+                echo "- N/A";
+                echo "EOF";
                 echo "branch_name=zed-zippy-${EXTENSION_ID}-autobump";
             } >> "$GITHUB_OUTPUT"
         fi
@@ -139,7 +150,7 @@ jobs:
         token: ${{ steps.generate-token.outputs.token }}
         sign-commits: true
         assignees: ${{ github.actor }}
-    timeout-minutes: 3
+    timeout-minutes: 5
     defaults:
       run:
         shell: bash -euxo pipefail {0}
@@ -160,6 +171,21 @@ jobs:
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
       with:
         clean: false
+    - id: determine-tag
+      name: extension_bump::determine_tag
+      run: |
+        EXTENSION_ID="$(sed -n 's/^id = "\(.*\)"/\1/p' < extension.toml | head -1 | tr -d '[:space:]')"
+
+        if [[ "$WORKING_DIR" == "." || -z "$WORKING_DIR" ]]; then
+            TAG="v${CURRENT_VERSION}"
+        else
+            TAG="${EXTENSION_ID}-v${CURRENT_VERSION}"
+        fi
+
+        echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
+      env:
+        CURRENT_VERSION: ${{ needs.check_version_changed.outputs.current_version }}
+        WORKING_DIR: ${{ inputs.working-directory }}
     - name: extension_bump::create_version_tag
       uses: actions/github-script@v7
       with:
@@ -167,10 +193,12 @@ jobs:
           github.rest.git.createRef({
               owner: context.repo.owner,
               repo: context.repo.repo,
-              ref: 'refs/tags/v${{ needs.check_version_changed.outputs.current_version }}',
+              ref: 'refs/tags/${{ steps.determine-tag.outputs.tag }}',
               sha: context.sha
           })
         github-token: ${{ steps.generate-token.outputs.token }}
+    outputs:
+      tag: ${{ steps.determine-tag.outputs.tag }}
     timeout-minutes: 1
     defaults:
       run:
@@ -202,11 +230,11 @@ jobs:
 
         echo "extension_id=${EXTENSION_ID}" >> "$GITHUB_OUTPUT"
     - name: extension_bump::release_action
-      uses: huacnlee/zed-extension-action@v2
+      uses: zed-extensions/update-action@543925fc45da8866b0d017218a656c8a3296ed3f
       with:
         extension-name: ${{ steps.get-extension-id.outputs.extension_id }}
         push-to: zed-industries/extensions
-        tag: v${{ needs.check_version_changed.outputs.current_version }}
+        tag: ${{ needs.create_version_label.outputs.tag }}
       env:
         COMMITTER_TOKEN: ${{ steps.generate-token.outputs.token }}
     defaults:

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

@@ -1,5 +1,6 @@
 # Labels pull requests by author: 'bot' for bot accounts, 'staff' for
-# staff team members, 'first contribution' for first-time external contributors.
+# staff team members, 'guild' for guild members, 'first contribution' for
+# first-time external contributors.
 name: PR Labeler
 
 on:
@@ -29,8 +30,47 @@ jobs:
           script: |
             const BOT_LABEL = 'bot';
             const STAFF_LABEL = 'staff';
+            const GUILD_LABEL = 'guild';
             const FIRST_CONTRIBUTION_LABEL = 'first contribution';
             const STAFF_TEAM_SLUG = 'staff';
+            const GUILD_MEMBERS = [
+              '11happy',
+              'AidanV',
+              'AmaanBilwar',
+              'OmChillure',
+              'Palanikannan1437',
+              'Shivansh-25',
+              'SkandaBhat',
+              'TwistingTwists',
+              'YEDASAVG',
+              'Ziqi-Yang',
+              'alanpjohn',
+              'arjunkomath',
+              'austincummings',
+              'ayushk-1801',
+              'claiwe',
+              'criticic',
+              'dongdong867',
+              'emamulandalib',
+              'eureka928',
+              'iam-liam',
+              'iksuddle',
+              'ishaksebsib',
+              'lingyaochu',
+              'marcocondrache',
+              'mchisolm0',
+              'nairadithya',
+              'nihalxkumar',
+              'notJoon',
+              'polyesterswing',
+              'prayanshchh',
+              'razeghi71',
+              'sarmadgulzar',
+              'seanstrom',
+              'th0jensen',
+              'tommyming',
+              'virajbhartiya',
+            ];
 
             const pr = context.payload.pull_request;
             const author = pr.user.login;
@@ -71,6 +111,17 @@ jobs:
               return;
             }
 
+            if (GUILD_MEMBERS.includes(author)) {
+              await github.rest.issues.addLabels({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                issue_number: pr.number,
+                labels: [GUILD_LABEL]
+              });
+              console.log(`PR #${pr.number} by ${author}: labeled '${GUILD_LABEL}' (guild member)`);
+              // No early return: guild members can also get 'first contribution'
+            }
+
             // We use inverted logic here due to a suspected GitHub bug where first-time contributors
             // get 'NONE' instead of 'FIRST_TIME_CONTRIBUTOR' or 'FIRST_TIMER'.
             // https://github.com/orgs/community/discussions/78038

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

@@ -72,8 +72,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::setup_node
@@ -199,8 +197,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::setup_sccache
@@ -318,8 +314,6 @@ jobs:
         token: ${{ secrets.SENTRY_AUTH_TOKEN }}
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: ./script/bundle-linux
@@ -360,8 +354,6 @@ jobs:
         token: ${{ secrets.SENTRY_AUTH_TOKEN }}
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: ./script/bundle-linux

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

@@ -122,8 +122,6 @@ jobs:
         token: ${{ secrets.SENTRY_AUTH_TOKEN }}
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: ./script/bundle-linux
@@ -170,8 +168,6 @@ jobs:
         token: ${{ secrets.SENTRY_AUTH_TOKEN }}
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: ./script/bundle-linux

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

@@ -34,8 +34,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::setup_cargo_config

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

@@ -32,8 +32,6 @@ jobs:
         token: ${{ secrets.SENTRY_AUTH_TOKEN }}
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: ./script/bundle-linux
@@ -73,8 +71,6 @@ jobs:
         token: ${{ secrets.SENTRY_AUTH_TOKEN }}
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: ./script/bundle-linux

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

@@ -35,8 +35,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::cargo_install_nextest

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

@@ -218,8 +218,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::setup_sccache
@@ -331,8 +329,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::setup_node
@@ -430,8 +426,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::setup_cargo_config
@@ -480,8 +474,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::setup_sccache
@@ -606,8 +598,6 @@ jobs:
         jobSummary: false
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: ./script/generate-action-metadata

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

@@ -38,8 +38,6 @@ jobs:
         path: ~/.rustup
     - name: steps::setup_linux
       run: ./script/linux
-    - name: steps::install_mold
-      run: ./script/install-mold
     - name: steps::download_wasi_sdk
       run: ./script/download-wasi-sdk
     - name: steps::cargo_install_nextest

Cargo.lock πŸ”—

@@ -6583,6 +6583,7 @@ dependencies = [
  "async-trait",
  "cocoa 0.26.0",
  "collections",
+ "dunce",
  "fs",
  "futures 0.3.31",
  "git",
@@ -7355,6 +7356,7 @@ dependencies = [
  "db",
  "editor",
  "feature_flags",
+ "file_icons",
  "futures 0.3.31",
  "fuzzy",
  "git",
@@ -9444,7 +9446,6 @@ dependencies = [
  "aws_http_client",
  "base64 0.22.1",
  "bedrock",
- "chrono",
  "client",
  "cloud_api_types",
  "cloud_llm_client",
@@ -9758,7 +9759,7 @@ dependencies = [
 [[package]]
 name = "libwebrtc"
 version = "0.3.26"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=37835f840d0070d45ac8b31cce6a6ae7aca3f459#37835f840d0070d45ac8b31cce6a6ae7aca3f459"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=cf4375b244ebb51702968df7fc36e192d0f45ad5#cf4375b244ebb51702968df7fc36e192d0f45ad5"
 dependencies = [
  "cxx",
  "glib",
@@ -9856,7 +9857,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
 [[package]]
 name = "livekit"
 version = "0.7.32"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=37835f840d0070d45ac8b31cce6a6ae7aca3f459#37835f840d0070d45ac8b31cce6a6ae7aca3f459"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=cf4375b244ebb51702968df7fc36e192d0f45ad5#cf4375b244ebb51702968df7fc36e192d0f45ad5"
 dependencies = [
  "base64 0.22.1",
  "bmrng",
@@ -9882,7 +9883,7 @@ dependencies = [
 [[package]]
 name = "livekit-api"
 version = "0.4.14"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=37835f840d0070d45ac8b31cce6a6ae7aca3f459#37835f840d0070d45ac8b31cce6a6ae7aca3f459"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=cf4375b244ebb51702968df7fc36e192d0f45ad5#cf4375b244ebb51702968df7fc36e192d0f45ad5"
 dependencies = [
  "base64 0.21.7",
  "futures-util",
@@ -9909,7 +9910,7 @@ dependencies = [
 [[package]]
 name = "livekit-protocol"
 version = "0.7.1"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=37835f840d0070d45ac8b31cce6a6ae7aca3f459#37835f840d0070d45ac8b31cce6a6ae7aca3f459"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=cf4375b244ebb51702968df7fc36e192d0f45ad5#cf4375b244ebb51702968df7fc36e192d0f45ad5"
 dependencies = [
  "futures-util",
  "livekit-runtime",
@@ -9925,7 +9926,7 @@ dependencies = [
 [[package]]
 name = "livekit-runtime"
 version = "0.4.0"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=37835f840d0070d45ac8b31cce6a6ae7aca3f459#37835f840d0070d45ac8b31cce6a6ae7aca3f459"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=cf4375b244ebb51702968df7fc36e192d0f45ad5#cf4375b244ebb51702968df7fc36e192d0f45ad5"
 dependencies = [
  "tokio",
  "tokio-stream",
@@ -10247,6 +10248,7 @@ dependencies = [
  "pretty_assertions",
  "pulldown-cmark 0.13.0",
  "settings",
+ "stacksafe",
  "theme",
  "ui",
  "urlencoding",
@@ -15711,6 +15713,7 @@ dependencies = [
  "edit_prediction",
  "edit_prediction_ui",
  "editor",
+ "feature_flags",
  "fs",
  "futures 0.3.31",
  "fuzzy",
@@ -19951,7 +19954,7 @@ dependencies = [
 [[package]]
 name = "webrtc-sys"
 version = "0.3.23"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=37835f840d0070d45ac8b31cce6a6ae7aca3f459#37835f840d0070d45ac8b31cce6a6ae7aca3f459"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=cf4375b244ebb51702968df7fc36e192d0f45ad5#cf4375b244ebb51702968df7fc36e192d0f45ad5"
 dependencies = [
  "cc",
  "cxx",
@@ -19965,7 +19968,7 @@ dependencies = [
 [[package]]
 name = "webrtc-sys-build"
 version = "0.3.13"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=37835f840d0070d45ac8b31cce6a6ae7aca3f459#37835f840d0070d45ac8b31cce6a6ae7aca3f459"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=cf4375b244ebb51702968df7fc36e192d0f45ad5#cf4375b244ebb51702968df7fc36e192d0f45ad5"
 dependencies = [
  "anyhow",
  "fs2",
@@ -22120,14 +22123,14 @@ dependencies = [
 
 [[package]]
 name = "zed_glsl"
-version = "0.2.0"
+version = "0.2.1"
 dependencies = [
  "zed_extension_api 0.1.0",
 ]
 
 [[package]]
 name = "zed_html"
-version = "0.3.0"
+version = "0.3.1"
 dependencies = [
  "zed_extension_api 0.7.0",
 ]

Cargo.toml πŸ”—

@@ -548,6 +548,7 @@ derive_more = { version = "2.1.1", features = [
 dirs = "4.0"
 documented = "0.9.1"
 dotenvy = "0.15.0"
+dunce = "1.0"
 ec4rs = "1.1"
 emojis = "0.6.1"
 env_logger = "0.11"
@@ -844,8 +845,8 @@ notify = { git = "https://github.com/zed-industries/notify.git", rev = "ce58c24c
 notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "ce58c24cad542c28e04ced02e20325a4ec28a31d" }
 windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
 calloop = { git = "https://github.com/zed-industries/calloop" }
-livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "37835f840d0070d45ac8b31cce6a6ae7aca3f459" }
-libwebrtc = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "37835f840d0070d45ac8b31cce6a6ae7aca3f459" }
+livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "cf4375b244ebb51702968df7fc36e192d0f45ad5" }
+libwebrtc = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "cf4375b244ebb51702968df7fc36e192d0f45ad5" }
 
 [profile.dev]
 split-debuginfo = "unpacked"

assets/icons/eye_off.svg πŸ”—

@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5248 9.52487C9.32192 9.74576 9.07604 9.92297 8.80229 10.0457C8.52854 10.1685 8.23269 10.2341 7.93287 10.2384C7.63305 10.2427 7.33543 10.1857 7.05826 10.0709C6.78109 9.95608 6.53019 9.78592 6.32115 9.57088C6.11211 9.35584 5.94929 9.10052 5.84242 8.82002C5.73556 8.53953 5.68693 8.23974 5.69959 7.93908C5.71225 7.63842 5.78588 7.34389 5.9159 7.07326C6.04593 6.80263 6.22978 6.56148 6.45605 6.36487" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.58521 3.93988C7.04677 3.8469 7.51825 3.80005 7.99115 3.80055C9.27177 3.80055 10.5219 4.18055 11.584 4.89055C12.6461 5.60055 13.472 6.61055 13.956 7.79055C13.9839 7.85737 13.9989 7.92893 14 8.00131C14.0011 8.07369 13.9882 8.14566 13.9622 8.21327C13.706 8.81927 13.3778 9.39377 12.9841 9.92417" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.48047 5.37988C2.85585 5.97555 2.36015 6.69431 2.02605 7.49388C1.90005 7.80488 1.90005 8.15188 2.02605 8.46288C2.52405 9.64988 3.35605 10.6599 4.41705 11.3699C5.47805 12.0799 6.72205 12.4559 7.99305 12.4559C9.01905 12.4559 10.0291 12.2019 10.9311 11.7179" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2 2L14 14" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/keymaps/default-linux.json πŸ”—

@@ -671,14 +671,15 @@
     }
   },
   {
-    "context": "WorkspaceSidebar",
+    "context": "ThreadsSidebar",
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-n": "multi_workspace::NewWorkspaceInWindow",
       "left": "agents_sidebar::CollapseSelectedEntry",
       "right": "agents_sidebar::ExpandSelectedEntry",
-      "enter": "menu::Confirm"
-    }
+      "enter": "menu::Confirm",
+      "shift-backspace": "agent::RemoveSelectedThread",
+    },
   },
   {
     "context": "Workspace && debugger_running",

assets/keymaps/default-macos.json πŸ”—

@@ -739,14 +739,15 @@
     }
   },
   {
-    "context": "WorkspaceSidebar",
+    "context": "ThreadsSidebar",
     "use_key_equivalents": true,
     "bindings": {
       "cmd-n": "multi_workspace::NewWorkspaceInWindow",
       "left": "agents_sidebar::CollapseSelectedEntry",
       "right": "agents_sidebar::ExpandSelectedEntry",
-      "enter": "menu::Confirm"
-    }
+      "enter": "menu::Confirm",
+      "shift-backspace": "agent::RemoveSelectedThread",
+    },
   },
   {
     "context": "Workspace && debugger_running",

assets/keymaps/default-windows.json πŸ”—

@@ -675,14 +675,15 @@
     }
   },
   {
-    "context": "WorkspaceSidebar",
+    "context": "ThreadsSidebar",
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-n": "multi_workspace::NewWorkspaceInWindow",
       "left": "agents_sidebar::CollapseSelectedEntry",
       "right": "agents_sidebar::ExpandSelectedEntry",
-      "enter": "menu::Confirm"
-    }
+      "enter": "menu::Confirm",
+      "shift-backspace": "agent::RemoveSelectedThread",
+    },
   },
   {
     "context": "ApplicationMenu",

assets/settings/default.json πŸ”—

@@ -898,6 +898,14 @@
     // Choices: label_color, icon
     // Default: icon
     "status_style": "icon",
+    // Whether to show file icons in the git panel.
+    //
+    // Default: false
+    "file_icons": false,
+    // Whether to show folder icons or chevrons for directories in the git panel.
+    //
+    // Default: true
+    "folder_icons": true,
     // What branch name to use if `init.defaultBranch` is not set
     //
     // Default: main
@@ -1083,6 +1091,10 @@
         "tools": {},
       },
     },
+    // Whether to start a new thread in the current local project or in a new Git worktree.
+    //
+    // Default: local_project
+    "new_thread_location": "local_project",
     // Where to show notifications when the agent has either completed
     // its response, or else needs confirmation before it can run a
     // tool action.

assets/themes/ayu/ayu.json πŸ”—

@@ -71,31 +71,31 @@
         "terminal.background": "#0d1016ff",
         "terminal.foreground": "#bfbdb6ff",
         "terminal.bright_foreground": "#bfbdb6ff",
-        "terminal.dim_foreground": "#0d1016ff",
+        "terminal.dim_foreground": "#85847fff",
         "terminal.ansi.black": "#0d1016ff",
         "terminal.ansi.bright_black": "#545557ff",
-        "terminal.ansi.dim_black": "#bfbdb6ff",
+        "terminal.ansi.dim_black": "#3a3b3cff",
         "terminal.ansi.red": "#ef7177ff",
         "terminal.ansi.bright_red": "#83353bff",
-        "terminal.ansi.dim_red": "#febab9ff",
+        "terminal.ansi.dim_red": "#a74f53ff",
         "terminal.ansi.green": "#aad84cff",
         "terminal.ansi.bright_green": "#567627ff",
-        "terminal.ansi.dim_green": "#d8eca8ff",
+        "terminal.ansi.dim_green": "#769735ff",
         "terminal.ansi.yellow": "#feb454ff",
         "terminal.ansi.bright_yellow": "#92582bff",
-        "terminal.ansi.dim_yellow": "#ffd9aaff",
+        "terminal.ansi.dim_yellow": "#b17d3aff",
         "terminal.ansi.blue": "#5ac1feff",
         "terminal.ansi.bright_blue": "#27618cff",
-        "terminal.ansi.dim_blue": "#b7dffeff",
+        "terminal.ansi.dim_blue": "#3e87b1ff",
         "terminal.ansi.magenta": "#39bae5ff",
         "terminal.ansi.bright_magenta": "#205a78ff",
-        "terminal.ansi.dim_magenta": "#addcf3ff",
+        "terminal.ansi.dim_magenta": "#2782a0ff",
         "terminal.ansi.cyan": "#95e5cbff",
         "terminal.ansi.bright_cyan": "#4c806fff",
-        "terminal.ansi.dim_cyan": "#cbf2e4ff",
+        "terminal.ansi.dim_cyan": "#68a08eff",
         "terminal.ansi.white": "#bfbdb6ff",
         "terminal.ansi.bright_white": "#fafafaff",
-        "terminal.ansi.dim_white": "#787876ff",
+        "terminal.ansi.dim_white": "#85847fff",
         "link_text.hover": "#5ac1feff",
         "conflict": "#feb454ff",
         "conflict.background": "#572815ff",
@@ -855,31 +855,31 @@
         "terminal.background": "#242835ff",
         "terminal.foreground": "#cccac2ff",
         "terminal.bright_foreground": "#cccac2ff",
-        "terminal.dim_foreground": "#242835ff",
+        "terminal.dim_foreground": "#8e8d87ff",
         "terminal.ansi.black": "#242835ff",
         "terminal.ansi.bright_black": "#67696eff",
-        "terminal.ansi.dim_black": "#cccac2ff",
+        "terminal.ansi.dim_black": "#48494dff",
         "terminal.ansi.red": "#f18779ff",
         "terminal.ansi.bright_red": "#833f3cff",
-        "terminal.ansi.dim_red": "#fec4baff",
+        "terminal.ansi.dim_red": "#a85e54ff",
         "terminal.ansi.green": "#d5fe80ff",
         "terminal.ansi.bright_green": "#75993cff",
-        "terminal.ansi.dim_green": "#ecffc1ff",
+        "terminal.ansi.dim_green": "#95b159ff",
         "terminal.ansi.yellow": "#fecf72ff",
         "terminal.ansi.bright_yellow": "#937237ff",
-        "terminal.ansi.dim_yellow": "#ffe7b9ff",
+        "terminal.ansi.dim_yellow": "#b1904fff",
         "terminal.ansi.blue": "#72cffeff",
         "terminal.ansi.bright_blue": "#336d8dff",
-        "terminal.ansi.dim_blue": "#c1e7ffff",
+        "terminal.ansi.dim_blue": "#4f90b1ff",
         "terminal.ansi.magenta": "#5bcde5ff",
         "terminal.ansi.bright_magenta": "#2b6c7bff",
-        "terminal.ansi.dim_magenta": "#b7e7f2ff",
+        "terminal.ansi.dim_magenta": "#3f8fa0ff",
         "terminal.ansi.cyan": "#95e5cbff",
         "terminal.ansi.bright_cyan": "#4c806fff",
-        "terminal.ansi.dim_cyan": "#cbf2e4ff",
+        "terminal.ansi.dim_cyan": "#68a08eff",
         "terminal.ansi.white": "#cccac2ff",
         "terminal.ansi.bright_white": "#fafafaff",
-        "terminal.ansi.dim_white": "#898a8aff",
+        "terminal.ansi.dim_white": "#8e8d87ff",
         "link_text.hover": "#72cffeff",
         "conflict": "#fecf72ff",
         "conflict.background": "#574018ff",

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

@@ -976,6 +976,30 @@ pub struct AcpThread {
     draft_prompt: Option<Vec<acp::ContentBlock>>,
     /// The initial scroll position for the thread view, set during session registration.
     ui_scroll_position: Option<gpui::ListOffset>,
+    /// Buffer for smooth text streaming. Holds text that has been received from
+    /// the model but not yet revealed in the UI. A timer task drains this buffer
+    /// gradually to create a fluid typing effect instead of choppy chunk-at-a-time
+    /// updates.
+    streaming_text_buffer: Option<StreamingTextBuffer>,
+}
+
+struct StreamingTextBuffer {
+    /// Text received from the model but not yet appended to the Markdown source.
+    pending: String,
+    /// The number of bytes to reveal per timer turn.
+    bytes_to_reveal_per_tick: usize,
+    /// The Markdown entity being streamed into.
+    target: Entity<Markdown>,
+    /// Timer task that periodically moves text from `pending` into `source`.
+    _reveal_task: Task<()>,
+}
+
+impl StreamingTextBuffer {
+    /// The number of milliseconds between each timer tick, controlling how quickly
+    /// text is revealed.
+    const TASK_UPDATE_MS: u64 = 16;
+    /// The time in milliseconds to reveal the entire pending text.
+    const REVEAL_TARGET: f32 = 200.0;
 }
 
 impl From<&AcpThread> for ActionLogTelemetry {
@@ -1137,6 +1161,7 @@ impl AcpThread {
             had_error: false,
             draft_prompt: None,
             ui_scroll_position: None,
+            streaming_text_buffer: None,
         }
     }
 
@@ -1182,6 +1207,10 @@ impl AcpThread {
             .unwrap_or_else(|| self.title.clone())
     }
 
+    pub fn has_provisional_title(&self) -> bool {
+        self.provisional_title.is_some()
+    }
+
     pub fn entries(&self) -> &[AgentThreadEntry] {
         &self.entries
     }
@@ -1343,6 +1372,7 @@ impl AcpThread {
             }) = last_entry
             && *existing_indented == indented
         {
+            Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
             *id = message_id.or(id.take());
             content.append(chunk.clone(), &language_registry, path_style, cx);
             chunks.push(chunk);
@@ -1379,8 +1409,20 @@ impl AcpThread {
         indented: bool,
         cx: &mut Context<Self>,
     ) {
-        let language_registry = self.project.read(cx).languages().clone();
         let path_style = self.project.read(cx).path_style(cx);
+
+        // For text chunks going to an existing Markdown block, buffer for smooth
+        // streaming instead of appending all at once which may feel more choppy.
+        if let acp::ContentBlock::Text(text_content) = &chunk {
+            if let Some(markdown) = self.streaming_markdown_target(is_thought, indented) {
+                let entries_len = self.entries.len();
+                cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
+                self.buffer_streaming_text(&markdown, text_content.text.clone(), cx);
+                return;
+            }
+        }
+
+        let language_registry = self.project.read(cx).languages().clone();
         let entries_len = self.entries.len();
         if let Some(last_entry) = self.entries.last_mut()
             && let AgentThreadEntry::AssistantMessage(AssistantMessage {
@@ -1391,6 +1433,7 @@ impl AcpThread {
             && *existing_indented == indented
         {
             let idx = entries_len - 1;
+            Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
             cx.emit(AcpThreadEvent::EntryUpdated(idx));
             match (chunks.last_mut(), is_thought) {
                 (Some(AssistantMessageChunk::Message { block }), false)
@@ -1425,7 +1468,134 @@ impl AcpThread {
         }
     }
 
+    fn streaming_markdown_target(
+        &self,
+        is_thought: bool,
+        indented: bool,
+    ) -> Option<Entity<Markdown>> {
+        let last_entry = self.entries.last()?;
+        if let AgentThreadEntry::AssistantMessage(AssistantMessage {
+            chunks,
+            indented: existing_indented,
+            ..
+        }) = last_entry
+            && *existing_indented == indented
+            && let [.., chunk] = chunks.as_slice()
+        {
+            match (chunk, is_thought) {
+                (
+                    AssistantMessageChunk::Message {
+                        block: ContentBlock::Markdown { markdown },
+                    },
+                    false,
+                )
+                | (
+                    AssistantMessageChunk::Thought {
+                        block: ContentBlock::Markdown { markdown },
+                    },
+                    true,
+                ) => Some(markdown.clone()),
+                _ => None,
+            }
+        } else {
+            None
+        }
+    }
+
+    /// Add text to the streaming buffer. If the target changed (e.g. switching
+    /// from thoughts to message text), flush the old buffer first.
+    fn buffer_streaming_text(
+        &mut self,
+        markdown: &Entity<Markdown>,
+        text: String,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(buffer) = &mut self.streaming_text_buffer {
+            if buffer.target.entity_id() == markdown.entity_id() {
+                buffer.pending.push_str(&text);
+
+                buffer.bytes_to_reveal_per_tick = (buffer.pending.len() as f32
+                    / StreamingTextBuffer::REVEAL_TARGET
+                    * StreamingTextBuffer::TASK_UPDATE_MS as f32)
+                    .ceil() as usize;
+                return;
+            }
+            Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
+        }
+
+        let target = markdown.clone();
+        let _reveal_task = self.start_streaming_reveal(cx);
+        let pending_len = text.len();
+        let bytes_to_reveal = (pending_len as f32 / StreamingTextBuffer::REVEAL_TARGET
+            * StreamingTextBuffer::TASK_UPDATE_MS as f32)
+            .ceil() as usize;
+        self.streaming_text_buffer = Some(StreamingTextBuffer {
+            pending: text,
+            bytes_to_reveal_per_tick: bytes_to_reveal,
+            target,
+            _reveal_task,
+        });
+    }
+
+    /// Flush all buffered streaming text into the Markdown entity immediately.
+    fn flush_streaming_text(
+        streaming_text_buffer: &mut Option<StreamingTextBuffer>,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(buffer) = streaming_text_buffer.take() {
+            if !buffer.pending.is_empty() {
+                buffer
+                    .target
+                    .update(cx, |markdown, cx| markdown.append(&buffer.pending, cx));
+            }
+        }
+    }
+
+    /// Spawns a foreground task that periodically drains
+    /// `streaming_text_buffer.pending` into the target `Markdown` entity,
+    /// producing smooth, continuous text output.
+    fn start_streaming_reveal(&self, cx: &mut Context<Self>) -> Task<()> {
+        cx.spawn(async move |this, cx| {
+            loop {
+                cx.background_executor()
+                    .timer(Duration::from_millis(StreamingTextBuffer::TASK_UPDATE_MS))
+                    .await;
+
+                let should_continue = this
+                    .update(cx, |this, cx| {
+                        let Some(buffer) = &mut this.streaming_text_buffer else {
+                            return false;
+                        };
+
+                        if buffer.pending.is_empty() {
+                            return true;
+                        }
+
+                        let pending_len = buffer.pending.len();
+
+                        let byte_boundary = buffer
+                            .pending
+                            .ceil_char_boundary(buffer.bytes_to_reveal_per_tick)
+                            .min(pending_len);
+
+                        buffer.target.update(cx, |markdown: &mut Markdown, cx| {
+                            markdown.append(&buffer.pending[..byte_boundary], cx);
+                            buffer.pending.drain(..byte_boundary);
+                        });
+
+                        true
+                    })
+                    .unwrap_or(false);
+
+                if !should_continue {
+                    break;
+                }
+            }
+        })
+    }
+
     fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
+        Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
         self.entries.push(entry);
         cx.emit(AcpThreadEvent::NewEntry);
     }
@@ -1970,6 +2140,8 @@ impl AcpThread {
 
                 match response {
                     Ok(r) => {
+                        Self::flush_streaming_text(&mut this.streaming_text_buffer, cx);
+
                         if r.stop_reason == acp::StopReason::MaxTokens {
                             this.had_error = true;
                             cx.emit(AcpThreadEvent::Error);
@@ -2022,6 +2194,8 @@ impl AcpThread {
                         Ok(Some(r))
                     }
                     Err(e) => {
+                        Self::flush_streaming_text(&mut this.streaming_text_buffer, cx);
+
                         this.had_error = true;
                         cx.emit(AcpThreadEvent::Error);
                         log::error!("Error in run turn: {:?}", e);
@@ -2039,6 +2213,7 @@ impl AcpThread {
         };
         self.connection.cancel(&self.session_id, cx);
 
+        Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
         self.mark_pending_tools_as_canceled();
 
         // Wait for the send task to complete
@@ -2103,6 +2278,7 @@ impl AcpThread {
             return Task::ready(Err(anyhow!("not supported")));
         };
 
+        Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
         let telemetry = ActionLogTelemetry::from(&*self);
         cx.spawn(async move |this, cx| {
             cx.update(|cx| truncate.run(id.clone(), cx)).await?;

crates/agent/src/thread.rs πŸ”—

@@ -2570,6 +2570,14 @@ impl Thread {
                 .is_some()
             {
                 _ = this.update(cx, |this, cx| this.set_title(title.into(), cx));
+            } else {
+                // Emit TitleUpdated even on failure so that the propagation
+                // chain (agent::Thread β†’ NativeAgent β†’ AcpThread) fires and
+                // clears any provisional title that was set before the turn.
+                _ = this.update(cx, |_, cx| {
+                    cx.emit(TitleUpdated);
+                    cx.notify();
+                });
             }
             _ = this.update(cx, |this, _| this.pending_title_generation = None);
         }));

crates/agent_servers/src/acp.rs πŸ”—

@@ -361,6 +361,102 @@ impl AcpConnection {
     pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
         &self.agent_capabilities.prompt_capabilities
     }
+
+    fn apply_default_config_options(
+        &self,
+        session_id: &acp::SessionId,
+        config_options: &Rc<RefCell<Vec<acp::SessionConfigOption>>>,
+        cx: &mut AsyncApp,
+    ) {
+        let name = self.server_name.clone();
+        let defaults_to_apply: Vec<_> = {
+            let config_opts_ref = config_options.borrow();
+            config_opts_ref
+                .iter()
+                .filter_map(|config_option| {
+                    let default_value = self.default_config_options.get(&*config_option.id.0)?;
+
+                    let is_valid = match &config_option.kind {
+                        acp::SessionConfigKind::Select(select) => match &select.options {
+                            acp::SessionConfigSelectOptions::Ungrouped(options) => options
+                                .iter()
+                                .any(|opt| &*opt.value.0 == default_value.as_str()),
+                            acp::SessionConfigSelectOptions::Grouped(groups) => {
+                                groups.iter().any(|g| {
+                                    g.options
+                                        .iter()
+                                        .any(|opt| &*opt.value.0 == default_value.as_str())
+                                })
+                            }
+                            _ => false,
+                        },
+                        _ => false,
+                    };
+
+                    if is_valid {
+                        let initial_value = match &config_option.kind {
+                            acp::SessionConfigKind::Select(select) => {
+                                Some(select.current_value.clone())
+                            }
+                            _ => None,
+                        };
+                        Some((
+                            config_option.id.clone(),
+                            default_value.clone(),
+                            initial_value,
+                        ))
+                    } else {
+                        log::warn!(
+                            "`{}` is not a valid value for config option `{}` in {}",
+                            default_value,
+                            config_option.id.0,
+                            name
+                        );
+                        None
+                    }
+                })
+                .collect()
+        };
+
+        for (config_id, default_value, initial_value) in defaults_to_apply {
+            cx.spawn({
+                let default_value_id = acp::SessionConfigValueId::new(default_value.clone());
+                let session_id = session_id.clone();
+                let config_id_clone = config_id.clone();
+                let config_opts = config_options.clone();
+                let conn = self.connection.clone();
+                async move |_| {
+                    let result = conn
+                        .set_session_config_option(acp::SetSessionConfigOptionRequest::new(
+                            session_id,
+                            config_id_clone.clone(),
+                            default_value_id,
+                        ))
+                        .await
+                        .log_err();
+
+                    if result.is_none() {
+                        if let Some(initial) = initial_value {
+                            let mut opts = config_opts.borrow_mut();
+                            if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id_clone) {
+                                if let acp::SessionConfigKind::Select(select) = &mut opt.kind {
+                                    select.current_value = initial;
+                                }
+                            }
+                        }
+                    }
+                }
+            })
+            .detach();
+
+            let mut opts = config_options.borrow_mut();
+            if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id) {
+                if let acp::SessionConfigKind::Select(select) = &mut opt.kind {
+                    select.current_value = acp::SessionConfigValueId::new(default_value);
+                }
+            }
+        }
+    }
 }
 
 impl Drop for AcpConnection {
@@ -471,89 +567,7 @@ impl AgentConnection for AcpConnection {
             }
 
             if let Some(config_opts) = config_options.as_ref() {
-                let defaults_to_apply: Vec<_> = {
-                    let config_opts_ref = config_opts.borrow();
-                    config_opts_ref
-                        .iter()
-                        .filter_map(|config_option| {
-                            let default_value = self.default_config_options.get(&*config_option.id.0)?;
-
-                            let is_valid = match &config_option.kind {
-                                acp::SessionConfigKind::Select(select) => match &select.options {
-                                    acp::SessionConfigSelectOptions::Ungrouped(options) => {
-                                        options.iter().any(|opt| &*opt.value.0 == default_value.as_str())
-                                    }
-                                    acp::SessionConfigSelectOptions::Grouped(groups) => groups
-                                        .iter()
-                                        .any(|g| g.options.iter().any(|opt| &*opt.value.0 == default_value.as_str())),
-                                    _ => false,
-                                },
-                                _ => false,
-                            };
-
-                            if is_valid {
-                                let initial_value = match &config_option.kind {
-                                    acp::SessionConfigKind::Select(select) => {
-                                        Some(select.current_value.clone())
-                                    }
-                                    _ => None,
-                                };
-                                Some((config_option.id.clone(), default_value.clone(), initial_value))
-                            } else {
-                                log::warn!(
-                                    "`{}` is not a valid value for config option `{}` in {}",
-                                    default_value,
-                                    config_option.id.0,
-                                    name
-                                );
-                                None
-                            }
-                        })
-                        .collect()
-                };
-
-                for (config_id, default_value, initial_value) in defaults_to_apply {
-                    cx.spawn({
-                        let default_value_id = acp::SessionConfigValueId::new(default_value.clone());
-                        let session_id = response.session_id.clone();
-                        let config_id_clone = config_id.clone();
-                        let config_opts = config_opts.clone();
-                        let conn = self.connection.clone();
-                        async move |_| {
-                            let result = conn
-                                .set_session_config_option(
-                                    acp::SetSessionConfigOptionRequest::new(
-                                        session_id,
-                                        config_id_clone.clone(),
-                                        default_value_id,
-                                    ),
-                                )
-                                .await
-                                .log_err();
-
-                            if result.is_none() {
-                                if let Some(initial) = initial_value {
-                                    let mut opts = config_opts.borrow_mut();
-                                    if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id_clone) {
-                                        if let acp::SessionConfigKind::Select(select) =
-                                            &mut opt.kind
-                                        {
-                                            select.current_value = initial;
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    })
-                    .detach();
-
-                    let mut opts = config_opts.borrow_mut();
-                    if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id) {
-                        if let acp::SessionConfigKind::Select(select) = &mut opt.kind {
-                            select.current_value = acp::SessionConfigValueId::new(default_value);
-                        }
-                    }
-                }
+                self.apply_default_config_options(&response.session_id, config_opts, cx);
             }
 
             let action_log = cx.new(|_| ActionLog::new(project.clone()));
@@ -641,7 +655,7 @@ impl AgentConnection for AcpConnection {
             },
         );
 
-        cx.spawn(async move |_| {
+        cx.spawn(async move |cx| {
             let response = match self
                 .connection
                 .load_session(
@@ -658,6 +672,11 @@ impl AgentConnection for AcpConnection {
 
             let (modes, models, config_options) =
                 config_state(response.modes, response.models, response.config_options);
+
+            if let Some(config_opts) = config_options.as_ref() {
+                self.apply_default_config_options(&session_id, config_opts, cx);
+            }
+
             if let Some(session) = self.sessions.borrow_mut().get_mut(&session_id) {
                 session.session_modes = modes;
                 session.models = models;
@@ -716,7 +735,7 @@ impl AgentConnection for AcpConnection {
             },
         );
 
-        cx.spawn(async move |_| {
+        cx.spawn(async move |cx| {
             let response = match self
                 .connection
                 .resume_session(
@@ -734,6 +753,11 @@ impl AgentConnection for AcpConnection {
 
             let (modes, models, config_options) =
                 config_state(response.modes, response.models, response.config_options);
+
+            if let Some(config_opts) = config_options.as_ref() {
+                self.apply_default_config_options(&session_id, config_opts, cx);
+            }
+
             if let Some(session) = self.sessions.borrow_mut().get_mut(&session_id) {
                 session.session_modes = modes;
                 session.models = models;

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

@@ -68,14 +68,17 @@ impl AddLlmProviderInput {
         let provider_name =
             single_line_input("Provider Name", provider.name(), None, 1, window, cx);
         let api_url = single_line_input("API URL", provider.api_url(), None, 2, window, cx);
-        let api_key = single_line_input(
-            "API Key",
-            "000000000000000000000000000000000000000000000000",
-            None,
-            3,
-            window,
-            cx,
-        );
+        let api_key = cx.new(|cx| {
+            InputField::new(
+                window,
+                cx,
+                "000000000000000000000000000000000000000000000000",
+            )
+            .label("API Key")
+            .tab_index(3)
+            .tab_stop(true)
+            .masked(true)
+        });
 
         Self {
             provider_name,

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

@@ -3098,48 +3098,49 @@ impl AgentPanel {
 
         let content = match &self.active_view {
             ActiveView::AgentThread { server_view } => {
-                let is_generating_title = server_view
-                    .read(cx)
-                    .as_native_thread(cx)
-                    .map_or(false, |t| t.read(cx).is_generating_title());
+                let server_view_ref = server_view.read(cx);
+                let is_generating_title = server_view_ref.as_native_thread(cx).is_some()
+                    && server_view_ref.parent_thread(cx).map_or(false, |tv| {
+                        tv.read(cx).thread.read(cx).has_provisional_title()
+                    });
 
-                if let Some(title_editor) = server_view
-                    .read(cx)
+                if let Some(title_editor) = server_view_ref
                     .parent_thread(cx)
                     .map(|r| r.read(cx).title_editor.clone())
                 {
-                    let container = div()
-                        .w_full()
-                        .on_action({
-                            let thread_view = server_view.downgrade();
-                            move |_: &menu::Confirm, window, cx| {
-                                if let Some(thread_view) = thread_view.upgrade() {
-                                    thread_view.focus_handle(cx).focus(window, cx);
-                                }
-                            }
-                        })
-                        .on_action({
-                            let thread_view = server_view.downgrade();
-                            move |_: &editor::actions::Cancel, window, cx| {
-                                if let Some(thread_view) = thread_view.upgrade() {
-                                    thread_view.focus_handle(cx).focus(window, cx);
-                                }
-                            }
-                        })
-                        .child(title_editor);
-
                     if is_generating_title {
-                        container
+                        Label::new("New Thread…")
+                            .color(Color::Muted)
+                            .truncate()
                             .with_animation(
                                 "generating_title",
                                 Animation::new(Duration::from_secs(2))
                                     .repeat()
                                     .with_easing(pulsating_between(0.4, 0.8)),
-                                |div, delta| div.opacity(delta),
+                                |label, delta| label.alpha(delta),
                             )
                             .into_any_element()
                     } else {
-                        container.into_any_element()
+                        div()
+                            .w_full()
+                            .on_action({
+                                let thread_view = server_view.downgrade();
+                                move |_: &menu::Confirm, window, cx| {
+                                    if let Some(thread_view) = thread_view.upgrade() {
+                                        thread_view.focus_handle(cx).focus(window, cx);
+                                    }
+                                }
+                            })
+                            .on_action({
+                                let thread_view = server_view.downgrade();
+                                move |_: &editor::actions::Cancel, window, cx| {
+                                    if let Some(thread_view) = thread_view.upgrade() {
+                                        thread_view.focus_handle(cx).focus(window, cx);
+                                    }
+                                }
+                            })
+                            .child(title_editor)
+                            .into_any_element()
                     }
                 } else {
                     Label::new(server_view.read(cx).title(cx))

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

@@ -1263,13 +1263,14 @@ impl ConnectionView {
                 }
             }
             AcpThreadEvent::EntryUpdated(index) => {
-                if let Some(entry_view_state) = self
-                    .thread_view(&thread_id)
-                    .map(|active| active.read(cx).entry_view_state.clone())
-                {
+                if let Some(active) = self.thread_view(&thread_id) {
+                    let entry_view_state = active.read(cx).entry_view_state.clone();
                     entry_view_state.update(cx, |view_state, cx| {
                         view_state.sync_entry(*index, thread, window, cx)
                     });
+                    active.update(cx, |active, cx| {
+                        active.auto_expand_streaming_thought(cx);
+                    });
                 }
             }
             AcpThreadEvent::EntriesRemoved(range) => {
@@ -1301,6 +1302,7 @@ impl ConnectionView {
                 if let Some(active) = self.thread_view(&thread_id) {
                     active.update(cx, |active, _cx| {
                         active.thread_retry_status.take();
+                        active.clear_auto_expand_tracking();
                     });
                 }
                 if is_subagent {

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

@@ -194,6 +194,7 @@ pub struct ThreadView {
     pub expanded_tool_calls: HashSet<agent_client_protocol::ToolCallId>,
     pub expanded_tool_call_raw_inputs: HashSet<agent_client_protocol::ToolCallId>,
     pub expanded_thinking_blocks: HashSet<(usize, usize)>,
+    auto_expanded_thinking_block: Option<(usize, usize)>,
     pub subagent_scroll_handles: RefCell<HashMap<agent_client_protocol::SessionId, ScrollHandle>>,
     pub edits_expanded: bool,
     pub plan_expanded: bool,
@@ -425,6 +426,7 @@ impl ThreadView {
             expanded_tool_calls: HashSet::default(),
             expanded_tool_call_raw_inputs: HashSet::default(),
             expanded_thinking_blocks: HashSet::default(),
+            auto_expanded_thinking_block: None,
             subagent_scroll_handles: RefCell::new(HashMap::default()),
             edits_expanded: false,
             plan_expanded: false,
@@ -628,6 +630,7 @@ impl ThreadView {
                 if let Some(AgentThreadEntry::UserMessage(user_message)) =
                     self.thread.read(cx).entries().get(event.entry_index)
                     && user_message.id.is_some()
+                    && !self.is_subagent()
                 {
                     self.editing_message = Some(event.entry_index);
                     cx.notify();
@@ -637,6 +640,7 @@ impl ThreadView {
                 if let Some(AgentThreadEntry::UserMessage(user_message)) =
                     self.thread.read(cx).entries().get(event.entry_index)
                     && user_message.id.is_some()
+                    && !self.is_subagent()
                 {
                     if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) {
                         self.editing_message = None;
@@ -646,7 +650,9 @@ impl ThreadView {
             }
             ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::SendImmediately) => {}
             ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
-                self.regenerate(event.entry_index, editor.clone(), window, cx);
+                if !self.is_subagent() {
+                    self.regenerate(event.entry_index, editor.clone(), window, cx);
+                }
             }
             ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
                 self.cancel_editing(&Default::default(), window, cx);
@@ -3790,14 +3796,12 @@ impl ThreadView {
                     .as_ref()
                     .is_some_and(|checkpoint| checkpoint.show);
 
-                let agent_name = self.agent_name.clone();
                 let is_subagent = self.is_subagent();
-
-                let non_editable_icon = || {
-                    IconButton::new("non_editable", IconName::PencilUnavailable)
-                        .icon_size(IconSize::Small)
-                        .icon_color(Color::Muted)
-                        .style(ButtonStyle::Transparent)
+                let is_editable = message.id.is_some() && !is_subagent;
+                let agent_name = if is_subagent {
+                    "subagents".into()
+                } else {
+                    self.agent_name.clone()
                 };
 
                 v_flex()
@@ -3818,8 +3822,8 @@ impl ThreadView {
                     .gap_1p5()
                     .w_full()
                     .children(rules_item)
-                    .children(message.id.clone().and_then(|message_id| {
-                        message.checkpoint.as_ref()?.show.then(|| {
+                    .when(is_editable && has_checkpoint_button, |this| {
+                        this.children(message.id.clone().map(|message_id| {
                             h_flex()
                                 .px_3()
                                 .gap_2()
@@ -3835,8 +3839,8 @@ impl ThreadView {
                                         }))
                                 )
                                 .child(Divider::horizontal())
-                        })
-                    }))
+                        }))
+                    })
                     .child(
                         div()
                             .relative()
@@ -3852,8 +3856,11 @@ impl ThreadView {
                                     })
                                     .border_color(cx.theme().colors().border)
                                     .map(|this| {
-                                        if is_subagent {
-                                            return this.border_dashed();
+                                        if !is_editable {
+                                            if is_subagent {
+                                                return this.border_dashed();
+                                            }
+                                            return this;
                                         }
                                         if editing && editor_focus {
                                             return this.border_color(focus_border);
@@ -3861,12 +3868,9 @@ impl ThreadView {
                                         if editing && !editor_focus {
                                             return this.border_dashed()
                                         }
-                                        if message.id.is_some() {
-                                            return this.shadow_md().hover(|s| {
-                                                s.border_color(focus_border.opacity(0.8))
-                                            });
-                                        }
-                                        this
+                                        this.shadow_md().hover(|s| {
+                                            s.border_color(focus_border.opacity(0.8))
+                                        })
                                     })
                                     .text_xs()
                                     .child(editor.clone().into_any_element())
@@ -3884,20 +3888,7 @@ impl ThreadView {
                                     .overflow_hidden();
 
                                 let is_loading_contents = self.is_loading_contents;
-                                if is_subagent {
-                                    this.child(
-                                        base_container.border_dashed().child(
-                                            non_editable_icon().tooltip(move |_, cx| {
-                                                Tooltip::with_meta(
-                                                    "Unavailable Editing",
-                                                    None,
-                                                    "Editing subagent messages is currently not supported.",
-                                                    cx,
-                                                )
-                                            }),
-                                        ),
-                                    )
-                                } else if message.id.is_some() {
+                                if is_editable {
                                     this.child(
                                         base_container
                                             .child(
@@ -3936,26 +3927,29 @@ impl ThreadView {
                                     this.child(
                                         base_container
                                             .border_dashed()
-                                            .child(
-                                                non_editable_icon()
-                                                    .tooltip(Tooltip::element({
-                                                        move |_, _| {
-                                                            v_flex()
-                                                                .gap_1()
-                                                                .child(Label::new("Unavailable Editing")).child(
-                                                                    div().max_w_64().child(
-                                                                        Label::new(format!(
-                                                                            "Editing previous messages is not available for {} yet.",
-                                                                            agent_name.clone()
-                                                                        ))
-                                                                        .size(LabelSize::Small)
-                                                                        .color(Color::Muted),
-                                                                    ),
-                                                                )
-                                                                .into_any_element()
-                                                        }
-                                                    }))
-                                            )
+                                            .child(IconButton::new("non_editable", IconName::PencilUnavailable)
+                                                .icon_size(IconSize::Small)
+                                                .icon_color(Color::Muted)
+                                                .style(ButtonStyle::Transparent)
+                                                .tooltip(Tooltip::element({
+                                                    let agent_name = agent_name.clone();
+                                                    move |_, _| {
+                                                        v_flex()
+                                                            .gap_1()
+                                                            .child(Label::new("Unavailable Editing"))
+                                                            .child(
+                                                                div().max_w_64().child(
+                                                                    Label::new(format!(
+                                                                        "Editing previous messages is not available for {} yet.",
+                                                                        agent_name
+                                                                    ))
+                                                                    .size(LabelSize::Small)
+                                                                    .color(Color::Muted),
+                                                                ),
+                                                            )
+                                                            .into_any_element()
+                                                    }
+                                                }))),
                                     )
                                 }
                             }),
@@ -4573,6 +4567,53 @@ impl ThreadView {
             .into_any_element()
     }
 
+    /// If the last entry's last chunk is a streaming thought block, auto-expand it.
+    /// Also collapses the previously auto-expanded block when a new one starts.
+    pub(crate) fn auto_expand_streaming_thought(&mut self, cx: &mut Context<Self>) {
+        let key = {
+            let thread = self.thread.read(cx);
+            if thread.status() != ThreadStatus::Generating {
+                return;
+            }
+            let entries = thread.entries();
+            let last_ix = entries.len().saturating_sub(1);
+            match entries.get(last_ix) {
+                Some(AgentThreadEntry::AssistantMessage(msg)) => match msg.chunks.last() {
+                    Some(AssistantMessageChunk::Thought { .. }) => {
+                        Some((last_ix, msg.chunks.len() - 1))
+                    }
+                    _ => None,
+                },
+                _ => None,
+            }
+        };
+
+        if let Some(key) = key {
+            if self.auto_expanded_thinking_block != Some(key) {
+                if let Some(old_key) = self.auto_expanded_thinking_block.replace(key) {
+                    self.expanded_thinking_blocks.remove(&old_key);
+                }
+                self.expanded_thinking_blocks.insert(key);
+                cx.notify();
+            }
+        } else if self.auto_expanded_thinking_block.is_some() {
+            // The last chunk is no longer a thought (model transitioned to responding),
+            // so collapse the previously auto-expanded block.
+            self.collapse_auto_expanded_thinking_block();
+            cx.notify();
+        }
+    }
+
+    fn collapse_auto_expanded_thinking_block(&mut self) {
+        if let Some(key) = self.auto_expanded_thinking_block.take() {
+            self.expanded_thinking_blocks.remove(&key);
+        }
+    }
+
+    pub(crate) fn clear_auto_expand_tracking(&mut self) {
+        self.auto_expanded_thinking_block = None;
+    }
+
     fn render_thinking_block(
         &self,
         entry_ix: usize,
@@ -4594,20 +4635,6 @@ impl ThreadView {
             .entry(entry_ix)
             .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
 
-        let thinking_content = {
-            div()
-                .id(("thinking-content", chunk_ix))
-                .when_some(scroll_handle, |this, scroll_handle| {
-                    this.track_scroll(&scroll_handle)
-                })
-                .text_ui_sm(cx)
-                .overflow_hidden()
-                .child(self.render_markdown(
-                    chunk,
-                    MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
-                ))
-        };
-
         v_flex()
             .gap_1()
             .child(
@@ -4663,11 +4690,19 @@ impl ThreadView {
             .when(is_open, |this| {
                 this.child(
                     div()
+                        .id(("thinking-content", chunk_ix))
                         .ml_1p5()
                         .pl_3p5()
                         .border_l_1()
                         .border_color(self.tool_card_border_color(cx))
-                        .child(thinking_content),
+                        .when_some(scroll_handle, |this, scroll_handle| {
+                            this.track_scroll(&scroll_handle)
+                        })
+                        .overflow_hidden()
+                        .child(self.render_markdown(
+                            chunk,
+                            MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
+                        )),
                 )
             })
             .into_any_element()

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

@@ -1,8 +1,12 @@
 use std::sync::Arc;
 
-use crate::{Agent, agent_connection_store::AgentConnectionStore, thread_history::ThreadHistory};
+use crate::{
+    Agent, RemoveSelectedThread, agent_connection_store::AgentConnectionStore,
+    thread_history::ThreadHistory,
+};
 use acp_thread::AgentSessionInfo;
 use agent::ThreadStore;
+use agent_client_protocol as acp;
 use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
 use editor::Editor;
 use fs::Fs;
@@ -109,6 +113,7 @@ pub struct ThreadsArchiveView {
     list_state: ListState,
     items: Vec<ArchiveListItem>,
     selection: Option<usize>,
+    hovered_index: Option<usize>,
     filter_editor: Entity<Editor>,
     _subscriptions: Vec<gpui::Subscription>,
     selected_agent_menu: PopoverMenuHandle<ContextMenu>,
@@ -152,6 +157,7 @@ impl ThreadsArchiveView {
             list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
             items: Vec::new(),
             selection: None,
+            hovered_index: None,
             filter_editor,
             _subscriptions: vec![filter_editor_subscription],
             selected_agent_menu: PopoverMenuHandle::default(),
@@ -272,6 +278,37 @@ impl ThreadsArchiveView {
         });
     }
 
+    fn delete_thread(&mut self, session_id: &acp::SessionId, cx: &mut Context<Self>) {
+        let Some(history) = &self.history else {
+            return;
+        };
+        if !history.read(cx).supports_delete() {
+            return;
+        }
+        let session_id = session_id.clone();
+        history.update(cx, |history, cx| {
+            history
+                .delete_session(&session_id, cx)
+                .detach_and_log_err(cx);
+        });
+    }
+
+    fn remove_selected_thread(
+        &mut self,
+        _: &RemoveSelectedThread,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(ix) = self.selection else {
+            return;
+        };
+        let Some(ArchiveListItem::Entry { session, .. }) = self.items.get(ix) else {
+            return;
+        };
+        let session_id = session.session_id.clone();
+        self.delete_thread(&session_id, cx);
+    }
+
     fn is_selectable_item(&self, ix: usize) -> bool {
         matches!(self.items.get(ix), Some(ArchiveListItem::Entry { .. }))
     }
@@ -377,9 +414,17 @@ impl ThreadsArchiveView {
                 highlight_positions,
             } => {
                 let is_selected = self.selection == Some(ix);
+                let hovered = self.hovered_index == Some(ix);
+                let supports_delete = self
+                    .history
+                    .as_ref()
+                    .map(|h| h.read(cx).supports_delete())
+                    .unwrap_or(false);
                 let title: SharedString =
                     session.title.clone().unwrap_or_else(|| "Untitled".into());
                 let session_info = session.clone();
+                let session_id_for_delete = session.session_id.clone();
+                let focus_handle = self.focus_handle.clone();
                 let highlight_positions = highlight_positions.clone();
 
                 let timestamp = session.created_at.or(session.updated_at).map(|entry_time| {
@@ -429,12 +474,45 @@ impl ThreadsArchiveView {
                             .gap_2()
                             .justify_between()
                             .child(title_label)
-                            .when_some(timestamp, |this, ts| {
-                                this.child(
-                                    Label::new(ts).size(LabelSize::Small).color(Color::Muted),
-                                )
+                            .when(!(hovered && supports_delete), |this| {
+                                this.when_some(timestamp, |this, ts| {
+                                    this.child(
+                                        Label::new(ts).size(LabelSize::Small).color(Color::Muted),
+                                    )
+                                })
                             }),
                     )
+                    .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
+                        if *is_hovered {
+                            this.hovered_index = Some(ix);
+                        } else if this.hovered_index == Some(ix) {
+                            this.hovered_index = None;
+                        }
+                        cx.notify();
+                    }))
+                    .end_slot::<IconButton>(if hovered && supports_delete {
+                        Some(
+                            IconButton::new("delete-thread", IconName::Trash)
+                                .icon_size(IconSize::Small)
+                                .icon_color(Color::Muted)
+                                .tooltip({
+                                    move |_window, cx| {
+                                        Tooltip::for_action_in(
+                                            "Delete Thread",
+                                            &RemoveSelectedThread,
+                                            &focus_handle,
+                                            cx,
+                                        )
+                                    }
+                                })
+                                .on_click(cx.listener(move |this, _, _, cx| {
+                                    this.delete_thread(&session_id_for_delete, cx);
+                                    cx.stop_propagation();
+                                })),
+                        )
+                    } else {
+                        None
+                    })
                     .on_click(cx.listener(move |this, _, window, cx| {
                         this.open_thread(session_info.clone(), window, cx);
                     }))
@@ -683,6 +761,7 @@ impl Render for ThreadsArchiveView {
             .on_action(cx.listener(Self::select_first))
             .on_action(cx.listener(Self::select_last))
             .on_action(cx.listener(Self::confirm))
+            .on_action(cx.listener(Self::remove_selected_thread))
             .size_full()
             .bg(cx.theme().colors().surface_background)
             .child(self.render_header(cx))

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

@@ -1,5 +1,5 @@
 use crate::threads_archive_view::{ThreadsArchiveView, ThreadsArchiveViewEvent};
-use crate::{Agent, AgentPanel, AgentPanelEvent, NewThread};
+use crate::{Agent, AgentPanel, AgentPanelEvent, NewThread, RemoveSelectedThread};
 use acp_thread::ThreadStatus;
 use action_log::DiffStats;
 use agent::ThreadStore;
@@ -87,6 +87,7 @@ struct ActiveThreadInfo {
     icon: IconName,
     icon_from_external_svg: Option<SharedString>,
     is_background: bool,
+    is_title_generating: bool,
     diff_stats: DiffStats,
 }
 
@@ -119,6 +120,7 @@ struct ThreadEntry {
     workspace: ThreadEntryWorkspace,
     is_live: bool,
     is_background: bool,
+    is_title_generating: bool,
     highlight_positions: Vec<usize>,
     worktree_name: Option<SharedString>,
     worktree_highlight_positions: Vec<usize>,
@@ -247,6 +249,7 @@ pub struct ThreadsPanel {
     selection: Option<usize>,
     focused_thread: Option<acp::SessionId>,
     active_entry_index: Option<usize>,
+    hovered_thread_index: Option<usize>,
     collapsed_groups: HashSet<PathList>,
     expanded_groups: HashMap<PathList, usize>,
     view: SidebarView,
@@ -366,6 +369,7 @@ impl ThreadsPanel {
             selection: None,
             focused_thread: None,
             active_entry_index: None,
+            hovered_thread_index: None,
             collapsed_groups: HashSet::new(),
             expanded_groups: HashMap::new(),
             view: SidebarView::default(),
@@ -472,6 +476,8 @@ impl ThreadsPanel {
                 let icon = thread_view_ref.agent_icon;
                 let icon_from_external_svg = thread_view_ref.agent_icon_from_external_svg.clone();
                 let title = thread.title();
+                let is_native = thread_view_ref.as_native_thread(cx).is_some();
+                let is_title_generating = is_native && thread.has_provisional_title();
                 let session_id = thread.session_id().clone();
                 let is_background = agent_panel_ref.is_background_thread(&session_id);
 
@@ -495,6 +501,7 @@ impl ThreadsPanel {
                     icon,
                     icon_from_external_svg,
                     is_background,
+                    is_title_generating,
                     diff_stats,
                 }
             })
@@ -612,6 +619,7 @@ impl ThreadsPanel {
                             workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                             is_live: false,
                             is_background: false,
+                            is_title_generating: false,
                             highlight_positions: Vec::new(),
                             worktree_name: None,
                             worktree_highlight_positions: Vec::new(),
@@ -665,6 +673,7 @@ impl ThreadsPanel {
                                 workspace: target_workspace.clone(),
                                 is_live: false,
                                 is_background: false,
+                                is_title_generating: false,
                                 highlight_positions: Vec::new(),
                                 worktree_name: Some(worktree_name.clone()),
                                 worktree_highlight_positions: Vec::new(),
@@ -695,6 +704,7 @@ impl ThreadsPanel {
                         thread.icon_from_external_svg = info.icon_from_external_svg.clone();
                         thread.is_live = true;
                         thread.is_background = info.is_background;
+                        thread.is_title_generating = info.is_title_generating;
                         thread.diff_stats = info.diff_stats;
                     }
                 }
@@ -1049,6 +1059,7 @@ impl ThreadsPanel {
             .end_hover_gradient_overlay(true)
             .end_hover_slot(
                 h_flex()
+                    .gap_1()
                     .when(workspace_count > 1, |this| {
                         this.child(
                             IconButton::new(
@@ -1603,11 +1614,41 @@ impl ThreadsPanel {
         }
     }
 
+    fn delete_thread(&mut self, session_id: &acp::SessionId, cx: &mut Context<Self>) {
+        let Some(thread_store) = ThreadStore::try_global(cx) else {
+            return;
+        };
+        thread_store.update(cx, |store, cx| {
+            store
+                .delete_thread(session_id.clone(), cx)
+                .detach_and_log_err(cx);
+        });
+    }
+
+    fn remove_selected_thread(
+        &mut self,
+        _: &RemoveSelectedThread,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(ix) = self.selection else {
+            return;
+        };
+        let Some(ListEntry::Thread(thread)) = self.contents.entries.get(ix) else {
+            return;
+        };
+        if thread.agent != Agent::NativeAgent {
+            return;
+        }
+        let session_id = thread.session_info.session_id.clone();
+        self.delete_thread(&session_id, cx);
+    }
+
     fn render_thread(
         &self,
         ix: usize,
         thread: &ThreadEntry,
-        is_selected: bool,
+        is_focused: bool,
         docked_right: bool,
         cx: &mut Context<Self>,
     ) -> AnyElement {
@@ -1623,6 +1664,12 @@ impl ThreadsPanel {
         let session_info = thread.session_info.clone();
         let thread_workspace = thread.workspace.clone();
 
+        let is_hovered = self.hovered_thread_index == Some(ix);
+        let is_selected = self.focused_thread.as_ref() == Some(&session_info.session_id);
+        let can_delete = thread.agent == Agent::NativeAgent;
+        let session_id_for_delete = thread.session_info.session_id.clone();
+        let focus_handle = self.focus_handle.clone();
+
         let id = SharedString::from(format!("thread-entry-{}", ix));
 
         let timestamp = thread
@@ -1662,6 +1709,7 @@ impl ThreadsPanel {
             .when_some(timestamp, |this, ts| this.timestamp(ts))
             .highlight_positions(thread.highlight_positions.to_vec())
             .status(thread.status)
+            .generating_title(thread.is_title_generating)
             .notified(has_notification)
             .when(thread.diff_stats.lines_added > 0, |this| {
                 this.added(thread.diff_stats.lines_added as usize)
@@ -1669,9 +1717,43 @@ impl ThreadsPanel {
             .when(thread.diff_stats.lines_removed > 0, |this| {
                 this.removed(thread.diff_stats.lines_removed as usize)
             })
-            .selected(self.focused_thread.as_ref() == Some(&session_info.session_id))
-            .focused(is_selected)
+            .selected(is_selected)
+            .focused(is_focused)
             .docked_right(docked_right)
+            .hovered(is_hovered)
+            .on_hover(cx.listener(move |this, is_hovered: &bool, _window, cx| {
+                if *is_hovered {
+                    this.hovered_thread_index = Some(ix);
+                } else if this.hovered_thread_index == Some(ix) {
+                    this.hovered_thread_index = None;
+                }
+                cx.notify();
+            }))
+            .when(is_hovered && can_delete, |this| {
+                this.action_slot(
+                    IconButton::new("delete-thread", IconName::Trash)
+                        .icon_size(IconSize::Small)
+                        .icon_color(Color::Muted)
+                        .tooltip({
+                            let focus_handle = focus_handle.clone();
+                            move |_window, cx| {
+                                Tooltip::for_action_in(
+                                    "Delete Thread",
+                                    &RemoveSelectedThread,
+                                    &focus_handle,
+                                    cx,
+                                )
+                            }
+                        })
+                        .on_click({
+                            let session_id = session_id_for_delete.clone();
+                            cx.listener(move |this, _, _window, cx| {
+                                this.delete_thread(&session_id, cx);
+                                cx.stop_propagation();
+                            })
+                        }),
+                )
+            })
             .on_click({
                 let agent = thread.agent.clone();
                 cx.listener(move |this, _, window, cx| {
@@ -1810,8 +1892,13 @@ impl ThreadsPanel {
             .into_any_element()
     }
 
-    fn render_thread_list_header(&self, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render_thread_list_header(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
         let has_query = self.has_filter_query(cx);
+        let docked_right = self.position(window, cx) == DockPosition::Right;
 
         h_flex()
             .h(Tab::container_height(cx))
@@ -1821,37 +1908,29 @@ impl ThreadsPanel {
             .border_b_1()
             .border_color(cx.theme().colors().border)
             .child(self.render_filter_input())
-            .when(has_query, |this| {
-                this.child(
-                    IconButton::new("clear_filter", IconName::Close)
-                        .shape(IconButtonShape::Square)
-                        .tooltip(Tooltip::text("Clear Search"))
-                        .on_click(cx.listener(|this, _, window, cx| {
-                            this.reset_filter_editor_text(window, cx);
-                            this.update_entries(cx);
-                        })),
-                )
-            })
-    }
-
-    fn render_thread_list_footer(&self, cx: &mut Context<Self>) -> impl IntoElement {
-        h_flex()
-            .p_1p5()
-            .border_t_1()
-            .border_color(cx.theme().colors().border_variant)
             .child(
-                Button::new("view-archive", "Archive")
-                    .full_width()
-                    .label_size(LabelSize::Small)
-                    .style(ButtonStyle::Outlined)
-                    .start_icon(
-                        Icon::new(IconName::Archive)
-                            .size(IconSize::XSmall)
-                            .color(Color::Muted),
-                    )
-                    .on_click(cx.listener(|this, _, window, cx| {
-                        this.show_archive(window, cx);
-                    })),
+                h_flex()
+                    .gap_0p5()
+                    .when(!docked_right, |this| this.pr_1p5())
+                    .when(has_query, |this| {
+                        this.child(
+                            IconButton::new("clear_filter", IconName::Close)
+                                .shape(IconButtonShape::Square)
+                                .tooltip(Tooltip::text("Clear Search"))
+                                .on_click(cx.listener(|this, _, window, cx| {
+                                    this.reset_filter_editor_text(window, cx);
+                                    this.update_entries(cx);
+                                })),
+                        )
+                    })
+                    .child(
+                        IconButton::new("archive", IconName::Archive)
+                            .icon_size(IconSize::Small)
+                            .tooltip(Tooltip::text("Archive"))
+                            .on_click(cx.listener(|this, _, window, cx| {
+                                this.show_archive(window, cx);
+                            })),
+                    ),
             )
     }
 }
@@ -2064,7 +2143,7 @@ impl Render for ThreadsPanel {
 
         v_flex()
             .id("workspace-sidebar")
-            .key_context("WorkspaceSidebar")
+            .key_context("ThreadsSidebar")
             .track_focus(&self.focus_handle)
             .on_action(cx.listener(Self::select_next))
             .on_action(cx.listener(Self::select_previous))
@@ -2076,12 +2155,13 @@ impl Render for ThreadsPanel {
             .on_action(cx.listener(Self::expand_selected_entry))
             .on_action(cx.listener(Self::collapse_selected_entry))
             .on_action(cx.listener(Self::cancel))
+            .on_action(cx.listener(Self::remove_selected_thread))
             .font(ui_font)
             .size_full()
             .bg(cx.theme().colors().surface_background)
             .map(|this| match self.view {
                 SidebarView::ThreadList => this
-                    .child(self.render_thread_list_header(cx))
+                    .child(self.render_thread_list_header(window, cx))
                     .child(
                         v_flex()
                             .relative()
@@ -2097,8 +2177,7 @@ impl Render for ThreadsPanel {
                             )
                             .when_some(sticky_header, |this, header| this.child(header))
                             .vertical_scrollbar_for(&self.list_state, window, cx),
-                    )
-                    .child(self.render_thread_list_footer(cx)),
+                    ),
                 SidebarView::Archive => {
                     if let Some(archive_view) = &self.archive_view {
                         this.child(archive_view.clone())
@@ -2643,6 +2722,7 @@ mod tests {
                     workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: false,
                     is_background: false,
+                    is_title_generating: false,
                     highlight_positions: Vec::new(),
                     worktree_name: None,
                     worktree_highlight_positions: Vec::new(),
@@ -2665,6 +2745,7 @@ mod tests {
                     workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: true,
                     is_background: false,
+                    is_title_generating: false,
                     highlight_positions: Vec::new(),
                     worktree_name: None,
                     worktree_highlight_positions: Vec::new(),
@@ -2687,6 +2768,7 @@ mod tests {
                     workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: true,
                     is_background: false,
+                    is_title_generating: false,
                     highlight_positions: Vec::new(),
                     worktree_name: None,
                     worktree_highlight_positions: Vec::new(),
@@ -2709,6 +2791,7 @@ mod tests {
                     workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: false,
                     is_background: false,
+                    is_title_generating: false,
                     highlight_positions: Vec::new(),
                     worktree_name: None,
                     worktree_highlight_positions: Vec::new(),
@@ -2731,6 +2814,7 @@ mod tests {
                     workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                     is_live: true,
                     is_background: true,
+                    is_title_generating: false,
                     highlight_positions: Vec::new(),
                     worktree_name: None,
                     worktree_highlight_positions: Vec::new(),

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

@@ -843,6 +843,16 @@ impl BufferDiffInner<Entity<language::Buffer>> {
                 .end
                 .saturating_sub(prev_unstaged_hunk_buffer_end);
             let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
+
+            // Clamp to the index text bounds. The overshoot mapping assumes that
+            // text between unstaged hunks is identical in the buffer and index.
+            // When the buffer has been edited since the diff was computed, anchor
+            // positions shift while diff_base_byte_range values don't, which can
+            // cause index_end to exceed index_text.len().
+            // See `test_stage_all_with_stale_buffer` which would hit an assert
+            // without these min calls
+            let index_end = index_end.min(index_text.len());
+            let index_start = index_start.min(index_end);
             let index_byte_range = index_start..index_end;
 
             let replacement_text = match new_status {
@@ -2678,6 +2688,51 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_stage_all_with_stale_buffer(cx: &mut TestAppContext) {
+        // Regression test for ZED-5R2: when the buffer is edited after the diff is
+        // computed but before staging, anchor positions shift while diff_base_byte_range
+        // values don't. If the primary (HEAD) hunk extends past the unstaged (index)
+        // hunk, an edit in the extension region shifts the primary hunk end without
+        // shifting the unstaged hunk end. The overshoot calculation then produces an
+        // index_end that exceeds index_text.len().
+        //
+        // Setup:
+        //   HEAD:   "aaa\nbbb\nccc\n"  (primary hunk covers lines 1-2)
+        //   Index:  "aaa\nbbb\nCCC\n"  (unstaged hunk covers line 1 only)
+        //   Buffer: "aaa\nBBB\nCCC\n"  (both lines differ from HEAD)
+        //
+        // The primary hunk spans buffer offsets 4..12, but the unstaged hunk only
+        // spans 4..8. The pending hunk extends 4 bytes past the unstaged hunk.
+        // An edit at offset 9 (inside "CCC") shifts the primary hunk end from 12
+        // to 13 but leaves the unstaged hunk end at 8, making index_end = 13 > 12.
+        let head_text = "aaa\nbbb\nccc\n";
+        let index_text = "aaa\nbbb\nCCC\n";
+        let buffer_text = "aaa\nBBB\nCCC\n";
+
+        let mut buffer = Buffer::new(
+            ReplicaId::LOCAL,
+            BufferId::new(1).unwrap(),
+            buffer_text.to_string(),
+        );
+
+        let unstaged_diff = cx.new(|cx| BufferDiff::new_with_base_text(index_text, &buffer, cx));
+        let uncommitted_diff = cx.new(|cx| {
+            let mut diff = BufferDiff::new_with_base_text(head_text, &buffer, cx);
+            diff.set_secondary_diff(unstaged_diff);
+            diff
+        });
+
+        // Edit the buffer in the region between the unstaged hunk end (offset 8)
+        // and the primary hunk end (offset 12). This shifts the primary hunk end
+        // but not the unstaged hunk end.
+        buffer.edit([(9..9, "Z")]);
+
+        uncommitted_diff.update(cx, |diff, cx| {
+            diff.stage_or_unstage_all_hunks(true, &buffer, true, cx);
+        });
+    }
+
     #[gpui::test]
     async fn test_toggling_stage_and_unstage_same_hunk(cx: &mut TestAppContext) {
         let head_text = "

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

@@ -41,7 +41,7 @@ use settings::{
 use std::collections::{VecDeque, hash_map};
 use std::env;
 use text::{AnchorRangeExt, Edit};
-use workspace::Workspace;
+use workspace::{AppState, Workspace};
 use zeta_prompt::{ZetaFormat, ZetaPromptInput};
 
 use std::mem;
@@ -1912,6 +1912,10 @@ impl EditPredictionStore {
             return;
         }
 
+        if currently_following(&project, cx) {
+            return;
+        }
+
         let Some(project_state) = self.projects.get_mut(&project.entity_id()) else {
             return;
         };
@@ -2048,6 +2052,25 @@ impl EditPredictionStore {
     pub const THROTTLE_TIMEOUT: Duration = Duration::from_millis(300);
 }
 
+fn currently_following(project: &Entity<Project>, cx: &App) -> bool {
+    let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade()) else {
+        return false;
+    };
+
+    app_state
+        .workspace_store
+        .read(cx)
+        .workspaces()
+        .filter_map(|workspace| workspace.upgrade())
+        .any(|workspace| {
+            workspace.read(cx).project().entity_id() == project.entity_id()
+                && workspace
+                    .read(cx)
+                    .leader_for_pane(workspace.read(cx).active_pane())
+                    .is_some()
+        })
+}
+
 fn is_ep_store_provider(provider: EditPredictionProvider) -> bool {
     match provider {
         EditPredictionProvider::Zed

crates/edit_prediction/src/edit_prediction_tests.rs πŸ”—

@@ -8,6 +8,7 @@ use cloud_llm_client::{
     EditPredictionRejectReason, EditPredictionRejection, RejectEditPredictionsBody,
     predict_edits_v3::{PredictEditsV3Request, PredictEditsV3Response},
 };
+
 use futures::{
     AsyncReadExt, FutureExt, StreamExt,
     channel::{mpsc, oneshot},
@@ -35,11 +36,12 @@ use util::{
     test::{TextRangeMarker, marked_text_ranges_by},
 };
 use uuid::Uuid;
+use workspace::{AppState, CollaboratorId, MultiWorkspace};
 use zeta_prompt::ZetaPromptInput;
 
 use crate::{
     BufferEditPrediction, EDIT_PREDICTION_SETTLED_QUIESCENCE, EditPredictionId,
-    EditPredictionStore, REJECT_REQUEST_DEBOUNCE,
+    EditPredictionJumpsFeatureFlag, EditPredictionStore, REJECT_REQUEST_DEBOUNCE,
 };
 
 #[gpui::test]
@@ -178,6 +180,172 @@ async fn test_current_state(cx: &mut TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_diagnostics_refresh_suppressed_while_following(cx: &mut TestAppContext) {
+    let (ep_store, mut requests) = init_test_with_fake_client(cx);
+
+    cx.update(|cx| {
+        cx.update_flags(
+            false,
+            vec![EditPredictionJumpsFeatureFlag::NAME.to_string()],
+        );
+    });
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/root",
+        json!({
+            "1.txt": "Hello!\nHow\nBye\n",
+            "2.txt": "Hola!\nComo\nAdios\n"
+        }),
+    )
+    .await;
+    let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await;
+
+    let app_state = cx.update(|cx| {
+        let app_state = AppState::test(cx);
+        AppState::set_global(Arc::downgrade(&app_state), cx);
+        app_state
+    });
+
+    let multi_workspace =
+        cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+    let workspace = multi_workspace
+        .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
+        .unwrap();
+    cx.update(|cx| {
+        AppState::set_global(Arc::downgrade(workspace.read(cx).app_state()), cx);
+    });
+    let _ = app_state;
+
+    let buffer1 = project
+        .update(cx, |project, cx| {
+            let path = project.find_project_path(path!("root/1.txt"), cx).unwrap();
+            project.set_active_path(Some(path.clone()), cx);
+            project.open_buffer(path, cx)
+        })
+        .await
+        .unwrap();
+    let snapshot1 = buffer1.read_with(cx, |buffer, _cx| buffer.snapshot());
+    let position = snapshot1.anchor_before(language::Point::new(1, 3));
+
+    ep_store.update(cx, |ep_store, cx| {
+        ep_store.register_project(&project, cx);
+        ep_store.register_buffer(&buffer1, &project, cx);
+        ep_store.refresh_prediction_from_buffer(project.clone(), buffer1.clone(), position, cx);
+    });
+
+    let (request, respond_tx) = requests.predict.next().await.unwrap();
+    respond_tx
+        .send(model_response(
+            &request,
+            indoc! {r"
+                --- a/root/1.txt
+                +++ b/root/1.txt
+                @@ ... @@
+                 Hello!
+                -How
+                +How are you?
+                 Bye
+            "},
+        ))
+        .unwrap();
+    cx.run_until_parked();
+
+    ep_store.update(cx, |ep_store, cx| {
+        ep_store.reject_current_prediction(EditPredictionRejectReason::Discarded, &project, cx);
+    });
+
+    let _ = multi_workspace.update(cx, |multi_workspace, window, cx| {
+        multi_workspace.workspace().update(cx, |workspace, cx| {
+            workspace.start_following(CollaboratorId::Agent, window, cx);
+        });
+    });
+    cx.run_until_parked();
+
+    let diagnostic = lsp::Diagnostic {
+        range: lsp::Range::new(lsp::Position::new(1, 1), lsp::Position::new(1, 5)),
+        severity: Some(lsp::DiagnosticSeverity::ERROR),
+        message: "Sentence is incomplete".to_string(),
+        ..Default::default()
+    };
+
+    project.update(cx, |project, cx| {
+        project.lsp_store().update(cx, |lsp_store, cx| {
+            lsp_store
+                .update_diagnostics(
+                    LanguageServerId(0),
+                    lsp::PublishDiagnosticsParams {
+                        uri: lsp::Uri::from_file_path(path!("/root/2.txt")).unwrap(),
+                        diagnostics: vec![diagnostic.clone()],
+                        version: None,
+                    },
+                    None,
+                    language::DiagnosticSourceKind::Pushed,
+                    &[],
+                    cx,
+                )
+                .unwrap();
+        });
+    });
+
+    cx.run_until_parked();
+    assert_no_predict_request_ready(&mut requests.predict);
+
+    let _ = multi_workspace.update(cx, |multi_workspace, window, cx| {
+        multi_workspace.workspace().update(cx, |workspace, cx| {
+            workspace.unfollow(CollaboratorId::Agent, window, cx);
+        });
+    });
+    cx.run_until_parked();
+
+    project.update(cx, |project, cx| {
+        project.lsp_store().update(cx, |lsp_store, cx| {
+            lsp_store
+                .update_diagnostics(
+                    LanguageServerId(0),
+                    lsp::PublishDiagnosticsParams {
+                        uri: lsp::Uri::from_file_path(path!("/root/2.txt")).unwrap(),
+                        diagnostics: vec![diagnostic],
+                        version: None,
+                    },
+                    None,
+                    language::DiagnosticSourceKind::Pushed,
+                    &[],
+                    cx,
+                )
+                .unwrap();
+        });
+    });
+
+    let (request, respond_tx) = requests.predict.next().await.unwrap();
+    respond_tx
+        .send(model_response(
+            &request,
+            indoc! {r#"
+                --- a/root/2.txt
+                +++ b/root/2.txt
+                @@ ... @@
+                 Hola!
+                -Como
+                +Como estas?
+                 Adios
+            "#},
+        ))
+        .unwrap();
+    cx.run_until_parked();
+
+    ep_store.update(cx, |ep_store, cx| {
+        let prediction = ep_store
+            .prediction_at(&buffer1, None, &project, cx)
+            .unwrap();
+        assert_matches!(
+            prediction,
+            BufferEditPrediction::Jump { prediction } if prediction.snapshot.file().unwrap().full_path(cx) == Path::new(path!("root/2.txt"))
+        );
+    });
+}
+
 #[gpui::test]
 async fn test_simple_request(cx: &mut TestAppContext) {
     let (ep_store, mut requests) = init_test_with_fake_client(cx);
@@ -2102,6 +2270,7 @@ fn empty_response() -> PredictEditsV3Response {
 
 fn prompt_from_request(request: &PredictEditsV3Request) -> String {
     zeta_prompt::format_zeta_prompt(&request.input, zeta_prompt::ZetaFormat::default())
+        .expect("default zeta prompt formatting should succeed in edit prediction tests")
 }
 
 fn assert_no_predict_request_ready(

crates/edit_prediction/src/zeta.rs πŸ”—

@@ -130,13 +130,14 @@ pub fn request_prediction_with_zeta(
                 return Err(anyhow::anyhow!("prompt contains special tokens"));
             }
 
+            let formatted_prompt = format_zeta_prompt(&prompt_input, zeta_version);
+
             if let Some(debug_tx) = &debug_tx {
-                let prompt = format_zeta_prompt(&prompt_input, zeta_version);
                 debug_tx
                     .unbounded_send(DebugEvent::EditPredictionStarted(
                         EditPredictionStartedDebugEvent {
                             buffer: buffer.downgrade(),
-                            prompt: Some(prompt),
+                            prompt: formatted_prompt.clone(),
                             position,
                         },
                     ))
@@ -145,11 +146,11 @@ pub fn request_prediction_with_zeta(
 
             log::trace!("Sending edit prediction request");
 
-            let (request_id, output, model_version, usage) =
-                if let Some(custom_settings) = &custom_server_settings {
+            let Some((request_id, output, model_version, usage)) =
+                (if let Some(custom_settings) = &custom_server_settings {
                     let max_tokens = custom_settings.max_output_tokens * 4;
 
-                    match custom_settings.prompt_format {
+                    Some(match custom_settings.prompt_format {
                         EditPredictionPromptFormat::Zeta => {
                             let ranges = &prompt_input.excerpt_ranges;
                             let editable_range_in_excerpt = ranges.editable_350.clone();
@@ -186,7 +187,9 @@ pub fn request_prediction_with_zeta(
                             (request_id, parsed_output, None, None)
                         }
                         EditPredictionPromptFormat::Zeta2 => {
-                            let prompt = format_zeta_prompt(&prompt_input, zeta_version);
+                            let Some(prompt) = formatted_prompt.clone() else {
+                                return Ok((None, None));
+                            };
                             let prefill = get_prefill(&prompt_input, zeta_version);
                             let prompt = format!("{prompt}{prefill}");
 
@@ -219,9 +222,11 @@ pub fn request_prediction_with_zeta(
                             (request_id, output_text, None, None)
                         }
                         _ => anyhow::bail!("unsupported prompt format"),
-                    }
+                    })
                 } else if let Some(config) = &raw_config {
-                    let prompt = format_zeta_prompt(&prompt_input, config.format);
+                    let Some(prompt) = format_zeta_prompt(&prompt_input, config.format) else {
+                        return Ok((None, None));
+                    };
                     let prefill = get_prefill(&prompt_input, config.format);
                     let prompt = format!("{prompt}{prefill}");
                     let environment = config
@@ -263,7 +268,7 @@ pub fn request_prediction_with_zeta(
                         None
                     };
 
-                    (request_id, output, None, usage)
+                    Some((request_id, output, None, usage))
                 } else {
                     // Use V3 endpoint - server handles model/version selection and suffix stripping
                     let (response, usage) = EditPredictionStore::send_v3_request(
@@ -284,8 +289,11 @@ pub fn request_prediction_with_zeta(
                         range_in_excerpt: response.editable_range,
                     };
 
-                    (request_id, Some(parsed_output), model_version, usage)
-                };
+                    Some((request_id, Some(parsed_output), model_version, usage))
+                })
+            else {
+                return Ok((None, None));
+            };
 
             let received_response_at = Instant::now();
 
@@ -296,7 +304,7 @@ pub fn request_prediction_with_zeta(
                 range_in_excerpt: editable_range_in_excerpt,
             }) = output
             else {
-                return Ok(((request_id, None), None));
+                return Ok((Some((request_id, None)), None));
             };
 
             let editable_range_in_buffer = editable_range_in_excerpt.start
@@ -342,7 +350,7 @@ pub fn request_prediction_with_zeta(
             );
 
             anyhow::Ok((
-                (
+                Some((
                     request_id,
                     Some(Prediction {
                         prompt_input,
@@ -354,14 +362,16 @@ pub fn request_prediction_with_zeta(
                         editable_range_in_buffer,
                         model_version,
                     }),
-                ),
+                )),
                 usage,
             ))
         }
     });
 
     cx.spawn(async move |this, cx| {
-        let (id, prediction) = handle_api_response(&this, request_task.await, cx)?;
+        let Some((id, prediction)) = handle_api_response(&this, request_task.await, cx)? else {
+            return Ok(None);
+        };
 
         let Some(Prediction {
             prompt_input: inputs,

crates/edit_prediction_cli/src/format_prompt.rs πŸ”—

@@ -92,7 +92,7 @@ pub async fn run_format_prompt(
                 zeta2_output_for_patch(prompt_inputs, patch, None, zeta_format).ok()
             });
 
-            example.prompt = Some(ExamplePrompt {
+            example.prompt = prompt.map(|prompt| ExamplePrompt {
                 input: prompt,
                 expected_output,
                 rejected_output,

crates/editor/src/edit_prediction_tests.rs πŸ”—

@@ -4,7 +4,13 @@ use edit_prediction_types::{
 use gpui::{Entity, KeyBinding, Modifiers, prelude::*};
 use indoc::indoc;
 use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
-use std::{ops::Range, sync::Arc};
+use std::{
+    ops::Range,
+    sync::{
+        Arc,
+        atomic::{self, AtomicUsize},
+    },
+};
 use text::{Point, ToOffset};
 use ui::prelude::*;
 
@@ -12,6 +18,8 @@ use crate::{
     AcceptEditPrediction, EditPrediction, MenuEditPredictionsPolicy, editor_tests::init_test,
     test::editor_test_context::EditorTestContext,
 };
+use rpc::proto::PeerId;
+use workspace::CollaboratorId;
 
 #[gpui::test]
 async fn test_edit_prediction_insert(cx: &mut gpui::TestAppContext) {
@@ -359,6 +367,60 @@ async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui:
     });
 }
 
+#[gpui::test]
+async fn test_edit_prediction_refresh_suppressed_while_following(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    let provider = cx.new(|_| FakeEditPredictionDelegate::default());
+    assign_editor_completion_provider(provider.clone(), &mut cx);
+    cx.set_state("let x = Λ‡;");
+
+    propose_edits(&provider, vec![(8..8, "42")], &mut cx);
+
+    cx.update_editor(|editor, window, cx| {
+        editor.refresh_edit_prediction(false, false, window, cx);
+        editor.update_visible_edit_prediction(window, cx);
+    });
+
+    assert_eq!(
+        provider.read_with(&cx.cx, |provider, _| {
+            provider.refresh_count.load(atomic::Ordering::SeqCst)
+        }),
+        1
+    );
+    cx.editor(|editor, _, _| {
+        assert!(editor.active_edit_prediction.is_some());
+    });
+
+    cx.update_editor(|editor, window, cx| {
+        editor.leader_id = Some(CollaboratorId::PeerId(PeerId::default()));
+        editor.refresh_edit_prediction(false, false, window, cx);
+    });
+
+    assert_eq!(
+        provider.read_with(&cx.cx, |provider, _| {
+            provider.refresh_count.load(atomic::Ordering::SeqCst)
+        }),
+        1
+    );
+    cx.editor(|editor, _, _| {
+        assert!(editor.active_edit_prediction.is_none());
+    });
+
+    cx.update_editor(|editor, window, cx| {
+        editor.leader_id = None;
+        editor.refresh_edit_prediction(false, false, window, cx);
+    });
+
+    assert_eq!(
+        provider.read_with(&cx.cx, |provider, _| {
+            provider.refresh_count.load(atomic::Ordering::SeqCst)
+        }),
+        2
+    );
+}
+
 #[gpui::test]
 async fn test_edit_prediction_preview_cleanup_on_toggle_off(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
@@ -567,6 +629,7 @@ fn assign_editor_completion_provider_non_zed(
 #[derive(Default, Clone)]
 pub struct FakeEditPredictionDelegate {
     pub completion: Option<edit_prediction_types::EditPrediction>,
+    pub refresh_count: Arc<AtomicUsize>,
 }
 
 impl FakeEditPredictionDelegate {
@@ -619,6 +682,7 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate {
         _debounce: bool,
         _cx: &mut gpui::Context<Self>,
     ) {
+        self.refresh_count.fetch_add(1, atomic::Ordering::SeqCst);
     }
 
     fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}

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

@@ -6481,6 +6481,7 @@ impl Editor {
             .selections
             .all::<MultiBufferOffset>(&self.display_snapshot(cx));
         let mut ranges = Vec::new();
+        let mut all_commit_ranges = Vec::new();
         let mut linked_edits = LinkedEdits::new();
 
         let text: Arc<str> = new_text.clone().into();
@@ -6506,10 +6507,12 @@ impl Editor {
 
             ranges.push(range.clone());
 
+            let start_anchor = snapshot.anchor_before(range.start);
+            let end_anchor = snapshot.anchor_after(range.end);
+            let anchor_range = start_anchor.text_anchor..end_anchor.text_anchor;
+            all_commit_ranges.push(anchor_range.clone());
+
             if !self.linked_edit_ranges.is_empty() {
-                let start_anchor = snapshot.anchor_before(range.start);
-                let end_anchor = snapshot.anchor_after(range.end);
-                let anchor_range = start_anchor.text_anchor..end_anchor.text_anchor;
                 linked_edits.push(&self, anchor_range, text.clone(), cx);
             }
         }
@@ -6596,6 +6599,7 @@ impl Editor {
             completions_menu.completions.clone(),
             candidate_id,
             true,
+            all_commit_ranges,
             cx,
         );
 
@@ -7804,7 +7808,11 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<()> {
-        let provider = self.edit_prediction_provider()?;
+        if self.leader_id.is_some() {
+            self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
+            return None;
+        }
+
         let cursor = self.selections.newest_anchor().head();
         let (buffer, cursor_buffer_position) =
             self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
@@ -7829,7 +7837,8 @@ impl Editor {
             return None;
         }
 
-        provider.refresh(buffer, cursor_buffer_position, debounce, cx);
+        self.edit_prediction_provider()?
+            .refresh(buffer, cursor_buffer_position, debounce, cx);
         Some(())
     }
 
@@ -7954,7 +7963,7 @@ impl Editor {
         cx: &App,
     ) -> bool {
         maybe!({
-            if self.read_only(cx) {
+            if self.read_only(cx) || self.leader_id.is_some() {
                 return Some(false);
             }
             let provider = self.edit_prediction_provider()?;
@@ -26570,6 +26579,7 @@ pub trait CompletionProvider {
         _completions: Rc<RefCell<Box<[Completion]>>>,
         _completion_index: usize,
         _push_to_history: bool,
+        _all_commit_ranges: Vec<Range<language::Anchor>>,
         _cx: &mut Context<Editor>,
     ) -> Task<Result<Option<language::Transaction>>> {
         Task::ready(Ok(None))
@@ -26938,6 +26948,7 @@ impl CompletionProvider for Entity<Project> {
         completions: Rc<RefCell<Box<[Completion]>>>,
         completion_index: usize,
         push_to_history: bool,
+        all_commit_ranges: Vec<Range<language::Anchor>>,
         cx: &mut Context<Editor>,
     ) -> Task<Result<Option<language::Transaction>>> {
         self.update(cx, |project, cx| {
@@ -26947,6 +26958,7 @@ impl CompletionProvider for Entity<Project> {
                     completions,
                     completion_index,
                     push_to_history,
+                    all_commit_ranges,
                     cx,
                 )
             })

crates/editor/src/editor_tests.rs πŸ”—

@@ -19888,6 +19888,100 @@ async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
     cx.assert_editor_state("fn main() { let a = Some(2)Λ‡; }");
 }
 
+#[gpui::test]
+async fn test_completions_with_additional_edits_and_multiple_cursors(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorLspTestContext::new_typescript(
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                resolve_provider: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
+
+    cx.set_state(
+        "import { «Fooˇ» } from './types';\n\nclass Bar {\n    method(): «Fooˇ» { return new Foo(); }\n}",
+    );
+
+    cx.simulate_keystroke("F");
+    cx.simulate_keystroke("o");
+
+    let completion_item = lsp::CompletionItem {
+        label: "FooBar".into(),
+        kind: Some(lsp::CompletionItemKind::CLASS),
+        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+            range: lsp::Range {
+                start: lsp::Position {
+                    line: 3,
+                    character: 14,
+                },
+                end: lsp::Position {
+                    line: 3,
+                    character: 16,
+                },
+            },
+            new_text: "FooBar".to_string(),
+        })),
+        additional_text_edits: Some(vec![lsp::TextEdit {
+            range: lsp::Range {
+                start: lsp::Position {
+                    line: 0,
+                    character: 9,
+                },
+                end: lsp::Position {
+                    line: 0,
+                    character: 11,
+                },
+            },
+            new_text: "FooBar".to_string(),
+        }]),
+        ..Default::default()
+    };
+
+    let closure_completion_item = completion_item.clone();
+    let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
+        let task_completion_item = closure_completion_item.clone();
+        async move {
+            Ok(Some(lsp::CompletionResponse::Array(vec![
+                task_completion_item,
+            ])))
+        }
+    });
+
+    request.next().await;
+
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
+    let apply_additional_edits = cx.update_editor(|editor, window, cx| {
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), window, cx)
+            .unwrap()
+    });
+
+    cx.assert_editor_state(
+        "import { FooBarˇ } from './types';\n\nclass Bar {\n    method(): FooBarˇ { return new Foo(); }\n}",
+    );
+
+    cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
+        let task_completion_item = completion_item.clone();
+        async move { Ok(task_completion_item) }
+    })
+    .next()
+    .await
+    .unwrap();
+
+    apply_additional_edits.await.unwrap();
+
+    cx.assert_editor_state(
+        "import { FooBarˇ } from './types';\n\nclass Bar {\n    method(): FooBarˇ { return new Foo(); }\n}",
+    );
+}
+
 #[gpui::test]
 async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
     init_test(cx, |_| {});

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

@@ -563,18 +563,21 @@ impl Matches {
                 .extend(history_items.into_iter().map(path_to_entry));
             return;
         };
-        // If several worktress are open we have to set the worktree root names in path prefix
-        let several_worktrees = worktree_store.read(cx).worktrees().count() > 1;
-        let worktree_name_by_id = several_worktrees.then(|| {
-            worktree_store
-                .read(cx)
-                .worktrees()
-                .map(|worktree| {
-                    let snapshot = worktree.read(cx).snapshot();
-                    (snapshot.id(), snapshot.root_name().into())
-                })
-                .collect()
-        });
+
+        let worktree_name_by_id = if should_hide_root_in_entry_path(&worktree_store, cx) {
+            None
+        } else {
+            Some(
+                worktree_store
+                    .read(cx)
+                    .worktrees()
+                    .map(|worktree| {
+                        let snapshot = worktree.read(cx).snapshot();
+                        (snapshot.id(), snapshot.root_name().into())
+                    })
+                    .collect(),
+            )
+        };
         let new_history_matches = matching_history_items(
             history_items,
             currently_opened,
@@ -797,6 +800,16 @@ fn matching_history_items<'a>(
     matching_history_paths
 }
 
+fn should_hide_root_in_entry_path(worktree_store: &Entity<WorktreeStore>, cx: &App) -> bool {
+    let multiple_worktrees = worktree_store
+        .read(cx)
+        .visible_worktrees(cx)
+        .filter(|worktree| !worktree.read(cx).is_single_file())
+        .nth(1)
+        .is_some();
+    ProjectPanelSettings::get_global(cx).hide_root && !multiple_worktrees
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 struct FoundPath {
     project: ProjectPath,
@@ -902,14 +915,12 @@ impl FileFinderDelegate {
             .currently_opened_path
             .as_ref()
             .map(|found_path| Arc::clone(&found_path.project.path));
-        let worktrees = self
-            .project
-            .read(cx)
-            .worktree_store()
+        let worktree_store = self.project.read(cx).worktree_store();
+        let worktrees = worktree_store
             .read(cx)
             .visible_worktrees_and_single_files(cx)
             .collect::<Vec<_>>();
-        let include_root_name = worktrees.len() > 1;
+        let include_root_name = !should_hide_root_in_entry_path(&worktree_store, cx);
         let candidate_sets = worktrees
             .into_iter()
             .map(|worktree| {
@@ -1135,17 +1146,8 @@ impl FileFinderDelegate {
                     if let Some(panel_match) = panel_match {
                         self.labels_for_path_match(&panel_match.0, path_style)
                     } else if let Some(worktree) = worktree {
-                        let multiple_folders_open = self
-                            .project
-                            .read(cx)
-                            .visible_worktrees(cx)
-                            .filter(|worktree| !worktree.read(cx).is_single_file())
-                            .nth(1)
-                            .is_some();
-
-                        let full_path = if ProjectPanelSettings::get_global(cx).hide_root
-                            && !multiple_folders_open
-                        {
+                        let worktree_store = self.project.read(cx).worktree_store();
+                        let full_path = if should_hide_root_in_entry_path(&worktree_store, cx) {
                             entry_path.project.path.clone()
                         } else {
                             worktree.read(cx).root_name().join(&entry_path.project.path)

crates/file_finder/src/file_finder_tests.rs πŸ”—

@@ -400,6 +400,18 @@ async fn test_absolute_paths(cx: &mut TestAppContext) {
 #[gpui::test]
 async fn test_complex_path(cx: &mut TestAppContext) {
     let app_state = init_test(cx);
+
+    cx.update(|cx| {
+        let settings = *ProjectPanelSettings::get_global(cx);
+        ProjectPanelSettings::override_global(
+            ProjectPanelSettings {
+                hide_root: true,
+                ..settings
+            },
+            cx,
+        );
+    });
+
     app_state
         .fs
         .as_fake()
@@ -1413,6 +1425,18 @@ async fn test_create_file_no_focused_with_multiple_worktrees(cx: &mut TestAppCon
 #[gpui::test]
 async fn test_path_distance_ordering(cx: &mut TestAppContext) {
     let app_state = init_test(cx);
+
+    cx.update(|cx| {
+        let settings = *ProjectPanelSettings::get_global(cx);
+        ProjectPanelSettings::override_global(
+            ProjectPanelSettings {
+                hide_root: true,
+                ..settings
+            },
+            cx,
+        );
+    });
+
     app_state
         .fs
         .as_fake()
@@ -1648,6 +1672,17 @@ async fn test_query_history(cx: &mut gpui::TestAppContext) {
 async fn test_history_match_positions(cx: &mut gpui::TestAppContext) {
     let app_state = init_test(cx);
 
+    cx.update(|cx| {
+        let settings = *ProjectPanelSettings::get_global(cx);
+        ProjectPanelSettings::override_global(
+            ProjectPanelSettings {
+                hide_root: true,
+                ..settings
+            },
+            cx,
+        );
+    });
+
     app_state
         .fs
         .as_fake()
@@ -2148,6 +2183,17 @@ async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
 async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
     let app_state = init_test(cx);
 
+    cx.update(|cx| {
+        let settings = *ProjectPanelSettings::get_global(cx);
+        ProjectPanelSettings::override_global(
+            ProjectPanelSettings {
+                hide_root: true,
+                ..settings
+            },
+            cx,
+        );
+    });
+
     app_state
         .fs
         .as_fake()
@@ -2253,6 +2299,17 @@ async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
 async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
     let app_state = init_test(cx);
 
+    cx.update(|cx| {
+        let settings = *ProjectPanelSettings::get_global(cx);
+        ProjectPanelSettings::override_global(
+            ProjectPanelSettings {
+                hide_root: true,
+                ..settings
+            },
+            cx,
+        );
+    });
+
     app_state
         .fs
         .as_fake()
@@ -2736,6 +2793,17 @@ async fn test_selected_history_item_stays_selected_on_worktree_updated(cx: &mut
 async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
     let app_state = init_test(cx);
 
+    cx.update(|cx| {
+        let settings = *ProjectPanelSettings::get_global(cx);
+        ProjectPanelSettings::override_global(
+            ProjectPanelSettings {
+                hide_root: true,
+                ..settings
+            },
+            cx,
+        );
+    });
+
     app_state
         .fs
         .as_fake()
@@ -2784,6 +2852,17 @@ async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppCo
 async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
     let app_state = init_test(cx);
 
+    cx.update(|cx| {
+        let settings = *ProjectPanelSettings::get_global(cx);
+        ProjectPanelSettings::override_global(
+            ProjectPanelSettings {
+                hide_root: true,
+                ..settings
+            },
+            cx,
+        );
+    });
+
     app_state
         .fs
         .as_fake()
@@ -3183,6 +3262,17 @@ async fn test_history_items_uniqueness_for_multiple_worktree_open_all_files(
 async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) {
     let app_state = init_test(cx);
 
+    cx.update(|cx| {
+        let settings = *ProjectPanelSettings::get_global(cx);
+        ProjectPanelSettings::override_global(
+            ProjectPanelSettings {
+                hide_root: true,
+                ..settings
+            },
+            cx,
+        );
+    });
+
     app_state.fs.as_fake().insert_tree("/src", json!({})).await;
 
     app_state
@@ -3779,6 +3869,17 @@ fn assert_match_at_position(
 async fn test_filename_precedence(cx: &mut TestAppContext) {
     let app_state = init_test(cx);
 
+    cx.update(|cx| {
+        let settings = *ProjectPanelSettings::get_global(cx);
+        ProjectPanelSettings::override_global(
+            ProjectPanelSettings {
+                hide_root: true,
+                ..settings
+            },
+            cx,
+        );
+    });
+
     app_state
         .fs
         .as_fake()
@@ -3823,6 +3924,18 @@ async fn test_filename_precedence(cx: &mut TestAppContext) {
 #[gpui::test]
 async fn test_paths_with_starting_slash(cx: &mut TestAppContext) {
     let app_state = init_test(cx);
+
+    cx.update(|cx| {
+        let settings = *ProjectPanelSettings::get_global(cx);
+        ProjectPanelSettings::override_global(
+            ProjectPanelSettings {
+                hide_root: true,
+                ..settings
+            },
+            cx,
+        );
+    });
+
     app_state
         .fs
         .as_fake()

crates/fs/Cargo.toml πŸ”—

@@ -48,6 +48,7 @@ cocoa = "0.26"
 
 [target.'cfg(target_os = "windows")'.dependencies]
 windows.workspace = true
+dunce.workspace = true
 
 [target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
 ashpd.workspace = true

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

@@ -37,7 +37,7 @@ use is_executable::IsExecutable;
 use rope::Rope;
 use serde::{Deserialize, Serialize};
 use smol::io::AsyncWriteExt;
-#[cfg(any(target_os = "windows", feature = "test-support"))]
+#[cfg(feature = "test-support")]
 use std::path::Component;
 use std::{
     io::{self, Write},
@@ -431,82 +431,43 @@ impl RealFs {
 
     #[cfg(target_os = "windows")]
     fn canonicalize(path: &Path) -> Result<PathBuf> {
-        let mut strip_prefix = None;
+        use std::ffi::OsString;
+        use std::os::windows::ffi::OsStringExt;
+        use windows::Win32::Storage::FileSystem::GetVolumePathNameW;
+        use windows::core::HSTRING;
 
-        let mut new_path = PathBuf::new();
-        for component in path.components() {
-            match component {
-                std::path::Component::Prefix(_) => {
-                    let component = component.as_os_str();
-                    let canonicalized = if component
-                        .to_str()
-                        .map(|e| e.ends_with("\\"))
-                        .unwrap_or(false)
-                    {
-                        std::fs::canonicalize(component)
-                    } else {
-                        let mut component = component.to_os_string();
-                        component.push("\\");
-                        std::fs::canonicalize(component)
-                    }?;
-
-                    let mut strip = PathBuf::new();
-                    for component in canonicalized.components() {
-                        match component {
-                            Component::Prefix(prefix_component) => {
-                                match prefix_component.kind() {
-                                    std::path::Prefix::Verbatim(os_str) => {
-                                        strip.push(os_str);
-                                    }
-                                    std::path::Prefix::VerbatimUNC(host, share) => {
-                                        strip.push("\\\\");
-                                        strip.push(host);
-                                        strip.push(share);
-                                    }
-                                    std::path::Prefix::VerbatimDisk(disk) => {
-                                        strip.push(format!("{}:", disk as char));
-                                    }
-                                    _ => strip.push(component),
-                                };
-                            }
-                            _ => strip.push(component),
-                        }
-                    }
-                    strip_prefix = Some(strip);
-                    new_path.push(component);
-                }
-                std::path::Component::RootDir => {
-                    new_path.push(component);
-                }
-                std::path::Component::CurDir => {
-                    if strip_prefix.is_none() {
-                        // unrooted path
-                        new_path.push(component);
-                    }
-                }
-                std::path::Component::ParentDir => {
-                    if strip_prefix.is_some() {
-                        // rooted path
-                        new_path.pop();
-                    } else {
-                        new_path.push(component);
-                    }
-                }
-                std::path::Component::Normal(_) => {
-                    if let Ok(link) = std::fs::read_link(new_path.join(component)) {
-                        let link = match &strip_prefix {
-                            Some(e) => link.strip_prefix(e).unwrap_or(&link),
-                            None => &link,
-                        };
-                        new_path.extend(link);
-                    } else {
-                        new_path.push(component);
-                    }
-                }
-            }
-        }
+        // std::fs::canonicalize resolves mapped network paths to UNC paths, which can
+        // confuse some software. To mitigate this, we canonicalize the input, then rebase
+        // the result onto the input's original volume root if both paths are on the same
+        // volume. This keeps the same drive letter or mount point the caller used.
 
-        Ok(new_path)
+        let abs_path = if path.is_relative() {
+            std::env::current_dir()?.join(path)
+        } else {
+            path.to_path_buf()
+        };
+
+        let path_hstring = HSTRING::from(abs_path.as_os_str());
+        let mut vol_buf = vec![0u16; abs_path.as_os_str().len() + 2];
+        unsafe { GetVolumePathNameW(&path_hstring, &mut vol_buf)? };
+        let volume_root = {
+            let len = vol_buf
+                .iter()
+                .position(|&c| c == 0)
+                .unwrap_or(vol_buf.len());
+            PathBuf::from(OsString::from_wide(&vol_buf[..len]))
+        };
+
+        let resolved_path = dunce::canonicalize(&abs_path)?;
+        let resolved_root = dunce::canonicalize(&volume_root)?;
+
+        if let Ok(relative) = resolved_path.strip_prefix(&resolved_root) {
+            let mut result = volume_root;
+            result.push(relative);
+            Ok(result)
+        } else {
+            Ok(resolved_path)
+        }
     }
 }
 

crates/git_ui/Cargo.toml πŸ”—

@@ -26,6 +26,7 @@ collections.workspace = true
 component.workspace = true
 db.workspace = true
 editor.workspace = true
+file_icons.workspace = true
 futures.workspace = true
 feature_flags.workspace = true
 fuzzy.workspace = true

crates/git_ui/src/git_panel.rs πŸ”—

@@ -20,6 +20,7 @@ use editor::{
     actions::ExpandAllDiffHunks,
 };
 use editor::{EditorStyle, RewrapOptions};
+use file_icons::FileIcons;
 use futures::StreamExt as _;
 use git::commit::ParsedCommitMessage;
 use git::repository::{
@@ -714,11 +715,16 @@ impl GitPanel {
 
             let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
             let mut was_tree_view = GitPanelSettings::get_global(cx).tree_view;
+            let mut was_file_icons = GitPanelSettings::get_global(cx).file_icons;
+            let mut was_folder_icons = GitPanelSettings::get_global(cx).folder_icons;
             let mut was_diff_stats = GitPanelSettings::get_global(cx).diff_stats;
             cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
-                let sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
-                let tree_view = GitPanelSettings::get_global(cx).tree_view;
-                let diff_stats = GitPanelSettings::get_global(cx).diff_stats;
+                let settings = GitPanelSettings::get_global(cx);
+                let sort_by_path = settings.sort_by_path;
+                let tree_view = settings.tree_view;
+                let file_icons = settings.file_icons;
+                let folder_icons = settings.folder_icons;
+                let diff_stats = settings.diff_stats;
                 if tree_view != was_tree_view {
                     this.view_mode = GitPanelViewMode::from_settings(cx);
                 }
@@ -731,12 +737,22 @@ impl GitPanel {
                 if (diff_stats != was_diff_stats) || update_entries {
                     this.update_visible_entries(window, cx);
                 }
+                if file_icons != was_file_icons || folder_icons != was_folder_icons {
+                    cx.notify();
+                }
                 was_sort_by_path = sort_by_path;
                 was_tree_view = tree_view;
+                was_file_icons = file_icons;
+                was_folder_icons = folder_icons;
                 was_diff_stats = diff_stats;
             })
             .detach();
 
+            cx.observe_global::<FileIcons>(|_, cx| {
+                cx.notify();
+            })
+            .detach();
+
             // just to let us render a placeholder editor.
             // Once the active git repo is set, this buffer will be replaced.
             let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
@@ -5020,15 +5036,21 @@ impl GitPanel {
         window: &Window,
         cx: &Context<Self>,
     ) -> AnyElement {
-        let tree_view = GitPanelSettings::get_global(cx).tree_view;
+        let settings = GitPanelSettings::get_global(cx);
+        let tree_view = settings.tree_view;
         let path_style = self.project.read(cx).path_style(cx);
         let git_path_style = ProjectSettings::get_global(cx).git.path_style;
         let display_name = entry.display_name(path_style);
 
         let selected = self.selected_entry == Some(ix);
         let marked = self.marked_entries.contains(&ix);
-        let status_style = GitPanelSettings::get_global(cx).status_style;
+        let status_style = settings.status_style;
         let status = entry.status;
+        let file_icon = if settings.file_icons {
+            FileIcons::get_icon(entry.repo_path.as_std_path(), cx)
+        } else {
+            None
+        };
 
         let has_conflict = status.is_conflicted();
         let is_modified = status.is_modified();
@@ -5105,6 +5127,21 @@ impl GitPanel {
             .min_w_0()
             .flex_1()
             .gap_1()
+            .when(settings.file_icons, |this| {
+                this.child(
+                    file_icon
+                        .map(|file_icon| {
+                            Icon::from_path(file_icon)
+                                .size(IconSize::Small)
+                                .color(Color::Muted)
+                        })
+                        .unwrap_or_else(|| {
+                            Icon::new(IconName::File)
+                                .size(IconSize::Small)
+                                .color(Color::Muted)
+                        }),
+                )
+            })
             .child(git_status_icon(status))
             .map(|this| {
                 if tree_view {
@@ -5273,10 +5310,24 @@ impl GitPanel {
             )
         };
 
-        let folder_icon = if entry.expanded {
-            IconName::FolderOpen
+        let settings = GitPanelSettings::get_global(cx);
+        let folder_icon = if settings.folder_icons {
+            FileIcons::get_folder_icon(entry.expanded, entry.key.path.as_std_path(), cx)
+        } else {
+            FileIcons::get_chevron_icon(entry.expanded, cx)
+        };
+        let fallback_folder_icon = if settings.folder_icons {
+            if entry.expanded {
+                IconName::FolderOpen
+            } else {
+                IconName::Folder
+            }
         } else {
-            IconName::Folder
+            if entry.expanded {
+                IconName::ChevronDown
+            } else {
+                IconName::ChevronRight
+            }
         };
 
         let stage_status = if let Some(repo) = &self.active_repository {
@@ -5299,9 +5350,17 @@ impl GitPanel {
             .gap_1()
             .pl(px(entry.depth as f32 * TREE_INDENT))
             .child(
-                Icon::new(folder_icon)
-                    .size(IconSize::Small)
-                    .color(Color::Muted),
+                folder_icon
+                    .map(|folder_icon| {
+                        Icon::from_path(folder_icon)
+                            .size(IconSize::Small)
+                            .color(Color::Muted)
+                    })
+                    .unwrap_or_else(|| {
+                        Icon::new(fallback_folder_icon)
+                            .size(IconSize::Small)
+                            .color(Color::Muted)
+                    }),
             )
             .child(self.entry_label(entry.name.clone(), label_color).truncate());
 

crates/git_ui/src/git_panel_settings.rs πŸ”—

@@ -20,6 +20,8 @@ pub struct GitPanelSettings {
     pub dock: DockPosition,
     pub default_width: Pixels,
     pub status_style: StatusStyle,
+    pub file_icons: bool,
+    pub folder_icons: bool,
     pub scrollbar: ScrollbarSettings,
     pub fallback_branch_name: String,
     pub sort_by_path: bool,
@@ -52,6 +54,8 @@ impl Settings for GitPanelSettings {
             dock: git_panel.dock.unwrap().into(),
             default_width: px(git_panel.default_width.unwrap()),
             status_style: git_panel.status_style.unwrap(),
+            file_icons: git_panel.file_icons.unwrap(),
+            folder_icons: git_panel.folder_icons.unwrap(),
             scrollbar: ScrollbarSettings {
                 show: git_panel.scrollbar.unwrap().show.map(Into::into),
             },

crates/gpui/src/text_system/line_wrapper.rs πŸ”—

@@ -236,6 +236,9 @@ impl LineWrapper {
         matches!(c, '\u{1E00}'..='\u{1EFF}') || // Latin Extended Additional
         matches!(c, '\u{0300}'..='\u{036F}') || // Combining Diacritical Marks
 
+        // Bengali (https://en.wikipedia.org/wiki/Bengali_(Unicode_block))
+        matches!(c, '\u{0980}'..='\u{09FF}') ||
+
         // Some other known special characters that should be treated as word characters,
         // e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`,
         // `2^3`, `a~b`, `a=1`, `Self::new`, etc.
@@ -856,6 +859,10 @@ mod tests {
         assert_word("ΠΠ‘Π’Π“Π”Π•Π–Π—Π˜Π™ΠšΠ›ΠœΠΠžΠŸ");
         // Vietnamese (https://github.com/zed-industries/zed/issues/23245)
         assert_word("ThαΊ­mchΓ­Δ‘αΊΏnkhithuachαΊ‘ychΓΊngcΓ²nnhαΊ«ntΓ’mgiαΊΏtnα»‘tsα»‘Δ‘Γ΄ngtΓΉchΓ­nhtrα»‹α»ŸYΓͺnBΓ‘ivΓ CaoBαΊ±ng");
+        // Bengali
+        assert_word("গিয়েছিলেন");
+        assert_word("ছেলে");
+        assert_word("ΰ¦Ήΰ¦šΰ§ΰ¦›ΰ¦Ώΰ¦²");
 
         // non-word characters
         assert_not_word("δ½ ε₯½");

crates/gpui_macos/src/window.rs πŸ”—

@@ -1799,10 +1799,13 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
             // may need them even if there is no marked text;
             // however we skip keys with control or the input handler adds control-characters to the buffer.
             // and keys with function, as the input handler swallows them.
+            // and keys with platform (Cmd), so that Cmd+key events (e.g. Cmd+`) are not
+            // consumed by the IME on non-QWERTY / dead-key layouts.
             if is_composing
                 || (key_down_event.keystroke.key_char.is_none()
                     && !key_down_event.keystroke.modifiers.control
-                    && !key_down_event.keystroke.modifiers.function)
+                    && !key_down_event.keystroke.modifiers.function
+                    && !key_down_event.keystroke.modifiers.platform)
             {
                 {
                     let mut lock = window_state.as_ref().lock();

crates/http_client/src/github_download.rs πŸ”—

@@ -155,6 +155,7 @@ async fn cleanup_staging_path(staging_path: &Path, asset_kind: AssetKind) {
 }
 
 async fn finalize_download(staging_path: &Path, destination_path: &Path) -> Result<()> {
+    _ = async_fs::remove_dir_all(destination_path).await;
     async_fs::rename(staging_path, destination_path)
         .await
         .with_context(|| format!("renaming {staging_path:?} to {destination_path:?}"))?;

crates/language_models/Cargo.toml πŸ”—

@@ -20,7 +20,6 @@ aws-credential-types = { workspace = true, features = ["hardcoded-credentials"]
 aws_http_client.workspace = true
 base64.workspace = true
 bedrock = { workspace = true, features = ["schemars"] }
-chrono.workspace = true
 client.workspace = true
 cloud_api_types.workspace = true
 cloud_llm_client.workspace = true

crates/language_models/src/provider/cloud.rs πŸ”—

@@ -1,7 +1,6 @@
 use ai_onboarding::YoungAccountBanner;
 use anthropic::AnthropicModelMode;
 use anyhow::{Context as _, Result, anyhow};
-use chrono::{DateTime, Utc};
 use client::{Client, UserStore, zed_urls};
 use cloud_api_types::{OrganizationId, Plan};
 use cloud_llm_client::{
@@ -1091,7 +1090,6 @@ fn response_lines<T: DeserializeOwned>(
 struct ZedAiConfiguration {
     is_connected: bool,
     plan: Option<Plan>,
-    subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
     eligible_for_trial: bool,
     account_too_young: bool,
     sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
@@ -1099,31 +1097,34 @@ struct ZedAiConfiguration {
 
 impl RenderOnce for ZedAiConfiguration {
     fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
-        let is_pro = self.plan.is_some_and(|plan| plan == Plan::ZedPro);
-        let subscription_text = match (self.plan, self.subscription_period) {
-            (Some(Plan::ZedPro), Some(_)) => {
-                "You have access to Zed's hosted models through your Pro subscription."
-            }
-            (Some(Plan::ZedProTrial), Some(_)) => {
-                "You have access to Zed's hosted models through your Pro trial."
-            }
-            (Some(Plan::ZedFree), Some(_)) => {
-                if self.eligible_for_trial {
-                    "Subscribe for access to Zed's hosted models. Start with a 14 day free trial."
-                } else {
-                    "Subscribe for access to Zed's hosted models."
-                }
-            }
-            _ => {
+        let (subscription_text, has_paid_plan) = match self.plan {
+            Some(Plan::ZedPro) => (
+                "You have access to Zed's hosted models through your Pro subscription.",
+                true,
+            ),
+            Some(Plan::ZedProTrial) => (
+                "You have access to Zed's hosted models through your Pro trial.",
+                false,
+            ),
+            Some(Plan::ZedStudent) => (
+                "You have access to Zed's hosted models through your Student subscription.",
+                true,
+            ),
+            Some(Plan::ZedBusiness) => (
+                "You have access to Zed's hosted models through your Organization.",
+                true,
+            ),
+            Some(Plan::ZedFree) | None => (
                 if self.eligible_for_trial {
                     "Subscribe for access to Zed's hosted models. Start with a 14 day free trial."
                 } else {
                     "Subscribe for access to Zed's hosted models."
-                }
-            }
+                },
+                false,
+            ),
         };
 
-        let manage_subscription_buttons = if is_pro {
+        let manage_subscription_buttons = if has_paid_plan {
             Button::new("manage_settings", "Manage Subscription")
                 .full_width()
                 .label_size(LabelSize::Small)
@@ -1207,7 +1208,6 @@ impl Render for ConfigurationView {
         ZedAiConfiguration {
             is_connected: !state.is_signed_out(cx),
             plan: user_store.plan(),
-            subscription_period: user_store.subscription_period(),
             eligible_for_trial: user_store.trial_started_at().is_none(),
             account_too_young: user_store.account_too_young(),
             sign_in_callback: self.sign_in_callback.clone(),
@@ -1238,9 +1238,6 @@ impl Component for ZedAiConfiguration {
             ZedAiConfiguration {
                 is_connected,
                 plan,
-                subscription_period: plan
-                    .is_some()
-                    .then(|| (Utc::now(), Utc::now() + chrono::Duration::days(7))),
                 eligible_for_trial,
                 account_too_young,
                 sign_in_callback: Arc::new(|_, _| {}),

crates/languages/src/cpp.rs πŸ”—

@@ -1,3 +1,15 @@
+use settings::SemanticTokenRules;
+
+use crate::LanguageDir;
+
+pub(crate) fn semantic_token_rules() -> SemanticTokenRules {
+    let content = LanguageDir::get("cpp/semantic_token_rules.json")
+        .expect("missing cpp/semantic_token_rules.json");
+    let json = std::str::from_utf8(&content.data).expect("invalid utf-8 in semantic_token_rules");
+    settings::parse_json_with_comments::<SemanticTokenRules>(json)
+        .expect("failed to parse cpp semantic_token_rules.json")
+}
+
 #[cfg(test)]
 mod tests {
     use gpui::{AppContext as _, BorrowAppContext, TestAppContext};

crates/languages/src/lib.rs πŸ”—

@@ -125,6 +125,7 @@ pub fn init(languages: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, node: NodeRuntime
         LanguageInfo {
             name: "cpp",
             adapters: vec![c_lsp_adapter],
+            semantic_token_rules: Some(cpp::semantic_token_rules()),
             ..Default::default()
         },
         LanguageInfo {

crates/languages/src/markdown/highlights.scm πŸ”—

@@ -21,7 +21,10 @@
   (list_marker_parenthesis)
 ] @punctuation.list_marker.markup
 
-(block_quote_marker) @punctuation.markup
+[
+  (block_quote_marker)
+  (block_continuation)
+] @punctuation.markup
 
 (pipe_table_header
   "|" @punctuation.markup)

crates/livekit_client/src/livekit_client/playback.rs πŸ”—

@@ -3,7 +3,7 @@ use anyhow::{Context as _, Result};
 use audio::{AudioSettings, CHANNEL_COUNT, LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE, SAMPLE_RATE};
 use cpal::DeviceId;
 use cpal::traits::{DeviceTrait, StreamTrait as _};
-use futures::channel::mpsc::UnboundedSender;
+use futures::channel::mpsc::Sender;
 use futures::{Stream, StreamExt as _};
 use gpui::{
     AsyncApp, BackgroundExecutor, Priority, ScreenCaptureFrame, ScreenCaptureSource,
@@ -201,7 +201,7 @@ impl AudioStack {
 
         let apm = self.apm.clone();
 
-        let (frame_tx, mut frame_rx) = futures::channel::mpsc::unbounded();
+        let (frame_tx, mut frame_rx) = futures::channel::mpsc::channel(1);
         let transmit_task = self.executor.spawn_with_priority(Priority::RealtimeAudio, {
             async move {
                 while let Some(frame) = frame_rx.next().await {
@@ -344,7 +344,7 @@ impl AudioStack {
     async fn capture_input(
         executor: BackgroundExecutor,
         apm: Arc<Mutex<apm::AudioProcessingModule>>,
-        frame_tx: UnboundedSender<AudioFrame<'static>>,
+        frame_tx: Sender<AudioFrame<'static>>,
         sample_rate: u32,
         num_channels: u32,
         input_audio_device: Option<DeviceId>,
@@ -354,7 +354,7 @@ impl AudioStack {
             let (device, config) = crate::default_device(true, input_audio_device.as_ref())?;
             let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>();
             let apm = apm.clone();
-            let frame_tx = frame_tx.clone();
+            let mut frame_tx = frame_tx.clone();
             let mut resampler = audio_resampler::AudioResampler::default();
 
             executor
@@ -408,7 +408,7 @@ impl AudioStack {
                                                 .log_err();
                                             buf.clear();
                                             frame_tx
-                                                .unbounded_send(AudioFrame {
+                                                .try_send(AudioFrame {
                                                     data: Cow::Owned(sampled),
                                                     sample_rate,
                                                     num_channels,
@@ -445,7 +445,7 @@ pub struct Speaker {
     pub sends_legacy_audio: bool,
 }
 
-fn send_to_livekit(frame_tx: UnboundedSender<AudioFrame<'static>>, mut microphone: impl Source) {
+fn send_to_livekit(mut frame_tx: Sender<AudioFrame<'static>>, mut microphone: impl Source) {
     use cpal::Sample;
     let sample_rate = microphone.sample_rate().get();
     let num_channels = microphone.channels().get() as u32;
@@ -458,17 +458,19 @@ fn send_to_livekit(frame_tx: UnboundedSender<AudioFrame<'static>>, mut microphon
             .map(|s| s.to_sample())
             .collect();
 
-        if frame_tx
-            .unbounded_send(AudioFrame {
-                sample_rate,
-                num_channels,
-                samples_per_channel: sampled.len() as u32 / num_channels,
-                data: Cow::Owned(sampled),
-            })
-            .is_err()
-        {
-            // must rx has dropped or is not consuming
-            break;
+        match frame_tx.try_send(AudioFrame {
+            sample_rate,
+            num_channels,
+            samples_per_channel: sampled.len() as u32 / num_channels,
+            data: Cow::Owned(sampled),
+        }) {
+            Ok(_) => {}
+            Err(err) => {
+                if !err.is_full() {
+                    // must rx has dropped or is not consuming
+                    break;
+                }
+            }
         }
     }
 }

crates/markdown_preview/Cargo.toml πŸ”—

@@ -30,6 +30,7 @@ markup5ever_rcdom.workspace = true
 pretty_assertions.workspace = true
 pulldown-cmark.workspace = true
 settings.workspace = true
+stacksafe.workspace = true
 theme.workspace = true
 ui.workspace = true
 urlencoding.workspace = true

crates/markdown_preview/src/markdown_parser.rs πŸ”—

@@ -10,6 +10,7 @@ use language::LanguageRegistry;
 use markdown::parser::PARSE_OPTIONS;
 use markup5ever_rcdom::RcDom;
 use pulldown_cmark::{Alignment, Event, Parser, Tag, TagEnd};
+use stacksafe::stacksafe;
 use std::{
     cell::RefCell, collections::HashMap, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec,
 };
@@ -907,6 +908,7 @@ impl<'a> MarkdownParser<'a> {
         elements
     }
 
+    #[stacksafe]
     fn parse_html_node(
         &self,
         source_range: Range<usize>,
@@ -1013,6 +1015,7 @@ impl<'a> MarkdownParser<'a> {
         }
     }
 
+    #[stacksafe]
     fn parse_paragraph(
         &self,
         source_range: Range<usize>,

crates/markdown_preview/src/markdown_preview_view.rs πŸ”—

@@ -277,6 +277,7 @@ impl MarkdownPreviewView {
             |this, editor, event: &EditorEvent, window, cx| {
                 match event {
                     EditorEvent::Edited { .. }
+                    | EditorEvent::BufferEdited { .. }
                     | EditorEvent::DirtyChanged
                     | EditorEvent::ExcerptsEdited { .. } => {
                         this.parse_markdown_from_active_editor(true, window, cx);

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

@@ -1,8 +1,5 @@
 use std::ops::Range;
-use std::{
-    cmp::{self, Reverse},
-    sync::Arc,
-};
+use std::{cmp, sync::Arc};
 
 use editor::scroll::ScrollOffset;
 use editor::{Anchor, AnchorRangeExt, Editor, scroll::Autoscroll};
@@ -183,11 +180,10 @@ impl OutlineView {
 struct OutlineViewDelegate {
     outline_view: WeakEntity<OutlineView>,
     active_editor: Entity<Editor>,
-    outline: Outline<Anchor>,
+    outline: Arc<Outline<Anchor>>,
     selected_match_index: usize,
     prev_scroll_position: Option<Point<ScrollOffset>>,
     matches: Vec<StringMatch>,
-    last_query: String,
 }
 
 enum OutlineRowHighlights {}
@@ -202,12 +198,11 @@ impl OutlineViewDelegate {
     ) -> Self {
         Self {
             outline_view,
-            last_query: Default::default(),
             matches: Default::default(),
             selected_match_index: 0,
             prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
             active_editor: editor,
-            outline,
+            outline: Arc::new(outline),
         }
     }
 
@@ -280,67 +275,73 @@ impl PickerDelegate for OutlineViewDelegate {
         window: &mut Window,
         cx: &mut Context<Picker<OutlineViewDelegate>>,
     ) -> Task<()> {
-        let selected_index;
-        if query.is_empty() {
+        let is_query_empty = query.is_empty();
+        if is_query_empty {
             self.restore_active_editor(window, cx);
-            self.matches = self
-                .outline
-                .items
-                .iter()
-                .enumerate()
-                .map(|(index, _)| StringMatch {
-                    candidate_id: index,
-                    score: Default::default(),
-                    positions: Default::default(),
-                    string: Default::default(),
-                })
-                .collect();
-
-            let (buffer, cursor_offset) = self.active_editor.update(cx, |editor, cx| {
-                let buffer = editor.buffer().read(cx).snapshot(cx);
-                let cursor_offset = editor
-                    .selections
-                    .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
-                    .head();
-                (buffer, cursor_offset)
-            });
-            selected_index = self
-                .outline
-                .items
-                .iter()
-                .enumerate()
-                .map(|(ix, item)| {
-                    let range = item.range.to_offset(&buffer);
-                    let distance_to_closest_endpoint = cmp::min(
-                        (range.start.0 as isize - cursor_offset.0 as isize).abs(),
-                        (range.end.0 as isize - cursor_offset.0 as isize).abs(),
-                    );
-                    let depth = if range.contains(&cursor_offset) {
-                        Some(item.depth)
-                    } else {
-                        None
-                    };
-                    (ix, depth, distance_to_closest_endpoint)
-                })
-                .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
-                .map(|(ix, _, _)| ix)
-                .unwrap_or(0);
-        } else {
-            self.matches = smol::block_on(
-                self.outline
-                    .search(&query, cx.background_executor().clone()),
-            );
-            selected_index = self
-                .matches
-                .iter()
-                .enumerate()
-                .max_by_key(|(_, m)| OrderedFloat(m.score))
-                .map(|(ix, _)| ix)
-                .unwrap_or(0);
         }
-        self.last_query = query;
-        self.set_selected_index(selected_index, !self.last_query.is_empty(), cx);
-        Task::ready(())
+
+        let outline = self.outline.clone();
+        cx.spawn_in(window, async move |this, cx| {
+            let matches = if is_query_empty {
+                outline
+                    .items
+                    .iter()
+                    .enumerate()
+                    .map(|(index, _)| StringMatch {
+                        candidate_id: index,
+                        score: Default::default(),
+                        positions: Default::default(),
+                        string: Default::default(),
+                    })
+                    .collect()
+            } else {
+                outline
+                    .search(&query, cx.background_executor().clone())
+                    .await
+            };
+
+            let _ = this.update(cx, |this, cx| {
+                this.delegate.matches = matches;
+                let selected_index = if is_query_empty {
+                    let (buffer, cursor_offset) =
+                        this.delegate.active_editor.update(cx, |editor, cx| {
+                            let snapshot = editor.display_snapshot(cx);
+                            let cursor_offset = editor
+                                .selections
+                                .newest::<MultiBufferOffset>(&snapshot)
+                                .head();
+                            (snapshot.buffer().clone(), cursor_offset)
+                        });
+                    this.delegate
+                        .matches
+                        .iter()
+                        .enumerate()
+                        .filter_map(|(ix, m)| {
+                            let item = &this.delegate.outline.items[m.candidate_id];
+                            let range = item.range.to_offset(&buffer);
+                            range.contains(&cursor_offset).then_some((ix, item.depth))
+                        })
+                        .max_by_key(|(ix, depth)| (*depth, cmp::Reverse(*ix)))
+                        .map(|(ix, _)| ix)
+                        .unwrap_or(0)
+                } else {
+                    this.delegate
+                        .matches
+                        .iter()
+                        .enumerate()
+                        .max_by(|(ix_a, a), (ix_b, b)| {
+                            OrderedFloat(a.score)
+                                .cmp(&OrderedFloat(b.score))
+                                .then(ix_b.cmp(ix_a))
+                        })
+                        .map(|(ix, _)| ix)
+                        .unwrap_or(0)
+                };
+
+                this.delegate
+                    .set_selected_index(selected_index, !is_query_empty, cx);
+            });
+        })
     }
 
     fn confirm(
@@ -586,6 +587,246 @@ mod tests {
         assert_single_caret_at_row(&editor, expected_first_highlighted_row, cx);
     }
 
+    #[gpui::test]
+    async fn test_outline_empty_query_prefers_deepest_containing_symbol_else_first(
+        cx: &mut TestAppContext,
+    ) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/dir"),
+            json!({
+                "a.rs": indoc! {"
+                                       // display line 0
+                    struct Outer {     // display line 1
+                        fn top(&self) {// display line 2
+                            let _x = 1;// display line 3
+                        }              // display line 4
+                    }                  // display line 5
+
+                    struct Another;    // display line 7
+                "}
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
+        project.read_with(cx, |project, _| {
+            project.languages().add(language::rust_lang())
+        });
+
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+
+        let workspace = cx.read(|cx| workspace.read(cx).workspace().clone());
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().update(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+        let _buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer(path!("/dir/a.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let editor = workspace
+            .update_in(cx, |workspace, window, cx| {
+                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        set_single_caret_at_row(&editor, 3, cx);
+        let outline_view = open_outline_view(&workspace, cx);
+        cx.run_until_parked();
+        let (selected_candidate_id, expected_deepest_containing_candidate_id) = outline_view
+            .update(cx, |outline_view, cx| {
+                let delegate = &outline_view.delegate;
+                let selected_candidate_id =
+                    delegate.matches[delegate.selected_match_index].candidate_id;
+                let (buffer, cursor_offset) = delegate.active_editor.update(cx, |editor, cx| {
+                    let buffer = editor.buffer().read(cx).snapshot(cx);
+                    let cursor_offset = editor
+                        .selections
+                        .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
+                        .head();
+                    (buffer, cursor_offset)
+                });
+                let deepest_containing_candidate_id = delegate
+                    .outline
+                    .items
+                    .iter()
+                    .enumerate()
+                    .filter_map(|(ix, item)| {
+                        item.range
+                            .to_offset(&buffer)
+                            .contains(&cursor_offset)
+                            .then_some((ix, item.depth))
+                    })
+                    .max_by(|(ix_a, depth_a), (ix_b, depth_b)| {
+                        depth_a.cmp(depth_b).then(ix_b.cmp(ix_a))
+                    })
+                    .map(|(ix, _)| ix)
+                    .unwrap();
+                (selected_candidate_id, deepest_containing_candidate_id)
+            });
+        assert_eq!(
+            selected_candidate_id, expected_deepest_containing_candidate_id,
+            "Empty query should select the deepest symbol containing the cursor"
+        );
+
+        cx.dispatch_action(menu::Cancel);
+        cx.run_until_parked();
+
+        set_single_caret_at_row(&editor, 0, cx);
+        let outline_view = open_outline_view(&workspace, cx);
+        cx.run_until_parked();
+        let selected_candidate_id = outline_view.read_with(cx, |outline_view, _| {
+            let delegate = &outline_view.delegate;
+            delegate.matches[delegate.selected_match_index].candidate_id
+        });
+        assert_eq!(
+            selected_candidate_id, 0,
+            "Empty query should fall back to the first symbol when cursor is outside all symbol ranges"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_outline_filtered_selection_prefers_first_match_on_score_ties(
+        cx: &mut TestAppContext,
+    ) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/dir"),
+            json!({
+                "a.rs": indoc! {"
+                    struct A;
+                    impl A {
+                        fn f(&self) {}
+                        fn g(&self) {}
+                    }
+
+                    struct B;
+                    impl B {
+                        fn f(&self) {}
+                        fn g(&self) {}
+                    }
+
+                    struct C;
+                    impl C {
+                        fn f(&self) {}
+                        fn g(&self) {}
+                    }
+                "}
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
+        project.read_with(cx, |project, _| {
+            project.languages().add(language::rust_lang())
+        });
+
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+
+        let workspace = cx.read(|cx| workspace.read(cx).workspace().clone());
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().update(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+        let _buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer(path!("/dir/a.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let editor = workspace
+            .update_in(cx, |workspace, window, cx| {
+                workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        assert_single_caret_at_row(&editor, 0, cx);
+        let outline_view = open_outline_view(&workspace, cx);
+        let match_ids = |outline_view: &Entity<Picker<OutlineViewDelegate>>,
+                         cx: &mut VisualTestContext| {
+            outline_view.read_with(cx, |outline_view, _| {
+                let delegate = &outline_view.delegate;
+                let selected_match = &delegate.matches[delegate.selected_match_index];
+                let scored_ids = delegate
+                    .matches
+                    .iter()
+                    .filter(|m| m.score > 0.0)
+                    .map(|m| m.candidate_id)
+                    .collect::<Vec<_>>();
+                (
+                    selected_match.candidate_id,
+                    *scored_ids.first().unwrap(),
+                    *scored_ids.last().unwrap(),
+                    scored_ids.len(),
+                )
+            })
+        };
+
+        outline_view
+            .update_in(cx, |outline_view, window, cx| {
+                outline_view
+                    .delegate
+                    .update_matches("f".to_string(), window, cx)
+            })
+            .await;
+        let (selected_id, first_scored_id, last_scored_id, scored_match_count) =
+            match_ids(&outline_view, cx);
+
+        assert!(
+            scored_match_count > 1,
+            "Expected multiple scored matches for `f` in outline filtering"
+        );
+        assert_eq!(
+            selected_id, first_scored_id,
+            "Filtered query should pick the first scored match when scores tie"
+        );
+        assert_ne!(
+            selected_id, last_scored_id,
+            "Selection should not default to the last scored match"
+        );
+
+        set_single_caret_at_row(&editor, 12, cx);
+        outline_view
+            .update_in(cx, |outline_view, window, cx| {
+                outline_view
+                    .delegate
+                    .update_matches("f".to_string(), window, cx)
+            })
+            .await;
+        let (selected_id, first_scored_id, last_scored_id, scored_match_count) =
+            match_ids(&outline_view, cx);
+
+        assert!(
+            scored_match_count > 1,
+            "Expected multiple scored matches for `f` in outline filtering"
+        );
+        assert_eq!(
+            selected_id, first_scored_id,
+            "Filtered selection should stay score-ordered and not switch based on cursor proximity"
+        );
+        assert_ne!(
+            selected_id, last_scored_id,
+            "Selection should not default to the last scored match"
+        );
+    }
+
     fn open_outline_view(
         workspace: &Entity<Workspace>,
         cx: &mut VisualTestContext,
@@ -634,6 +875,18 @@ mod tests {
         })
     }
 
+    fn set_single_caret_at_row(
+        editor: &Entity<Editor>,
+        buffer_row: u32,
+        cx: &mut VisualTestContext,
+    ) {
+        editor.update_in(cx, |editor, window, cx| {
+            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+                s.select_ranges([rope::Point::new(buffer_row, 0)..rope::Point::new(buffer_row, 0)])
+            });
+        });
+    }
+
     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
         cx.update(|cx| {
             let state = AppState::test(cx);

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

@@ -30,7 +30,6 @@ pub struct PlatformTitleBar {
     platform_style: PlatformStyle,
     children: SmallVec<[AnyElement; 2]>,
     should_move: bool,
-    background_color: Option<Hsla>,
     system_window_tabs: Entity<SystemWindowTabs>,
 }
 
@@ -44,16 +43,11 @@ impl PlatformTitleBar {
             platform_style,
             children: SmallVec::new(),
             should_move: false,
-            background_color: None,
             system_window_tabs,
         }
     }
 
     pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
-        if let Some(background_color) = self.background_color {
-            return background_color;
-        }
-
         if cfg!(any(target_os = "linux", target_os = "freebsd")) {
             if window.is_window_active() && !self.should_move {
                 cx.theme().colors().title_bar_background
@@ -72,10 +66,6 @@ impl PlatformTitleBar {
         self.children = children.into_iter().collect();
     }
 
-    pub fn set_background_color(&mut self, background_color: Option<Hsla>) {
-        self.background_color = background_color;
-    }
-
     pub fn init(cx: &mut App) {
         SystemWindowTabs::init(cx);
     }

crates/project/src/lsp_command.rs πŸ”—

@@ -4857,9 +4857,14 @@ impl LspCommand for GetFoldingRanges {
         self,
         message: proto::GetFoldingRangesResponse,
         _: Entity<LspStore>,
-        _: Entity<Buffer>,
-        _: AsyncApp,
+        buffer: Entity<Buffer>,
+        mut cx: AsyncApp,
     ) -> Result<Self::Response> {
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })
+            .await?;
         message
             .ranges
             .into_iter()

crates/project/src/lsp_store.rs πŸ”—

@@ -6643,6 +6643,7 @@ impl LspStore {
         completions: Rc<RefCell<Box<[Completion]>>>,
         completion_index: usize,
         push_to_history: bool,
+        all_commit_ranges: Vec<Range<language::Anchor>>,
         cx: &mut Context<Self>,
     ) -> Task<Result<Option<Transaction>>> {
         if let Some((client, project_id)) = self.upstream_client() {
@@ -6659,6 +6660,11 @@ impl LspStore {
                             new_text: completion.new_text,
                             source: completion.source,
                         })),
+                        all_commit_ranges: all_commit_ranges
+                            .iter()
+                            .cloned()
+                            .map(language::proto::serialize_anchor_range)
+                            .collect(),
                     }
                 };
 
@@ -6752,12 +6758,15 @@ impl LspStore {
                             let has_overlap = if is_file_start_auto_import {
                                 false
                             } else {
-                                let start_within = primary.start.cmp(&range.start, buffer).is_le()
-                                    && primary.end.cmp(&range.start, buffer).is_ge();
-                                let end_within = range.start.cmp(&primary.end, buffer).is_le()
-                                    && range.end.cmp(&primary.end, buffer).is_ge();
-                                let result = start_within || end_within;
-                                result
+                                all_commit_ranges.iter().any(|commit_range| {
+                                    let start_within =
+                                        commit_range.start.cmp(&range.start, buffer).is_le()
+                                            && commit_range.end.cmp(&range.start, buffer).is_ge();
+                                    let end_within =
+                                        range.start.cmp(&commit_range.end, buffer).is_le()
+                                            && range.end.cmp(&commit_range.end, buffer).is_ge();
+                                    start_within || end_within
+                                })
                             };
 
                             //Skip additional edits which overlap with the primary completion edit
@@ -10418,13 +10427,19 @@ impl LspStore {
         envelope: TypedEnvelope<proto::ApplyCompletionAdditionalEdits>,
         mut cx: AsyncApp,
     ) -> Result<proto::ApplyCompletionAdditionalEditsResponse> {
-        let (buffer, completion) = this.update(&mut cx, |this, cx| {
+        let (buffer, completion, all_commit_ranges) = this.update(&mut cx, |this, cx| {
             let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
             let buffer = this.buffer_store.read(cx).get_existing(buffer_id)?;
             let completion = Self::deserialize_completion(
                 envelope.payload.completion.context("invalid completion")?,
             )?;
-            anyhow::Ok((buffer, completion))
+            let all_commit_ranges = envelope
+                .payload
+                .all_commit_ranges
+                .into_iter()
+                .map(language::proto::deserialize_anchor_range)
+                .collect::<Result<Vec<_>, _>>()?;
+            anyhow::Ok((buffer, completion, all_commit_ranges))
         })?;
 
         let apply_additional_edits = this.update(&mut cx, |this, cx| {
@@ -10444,6 +10459,7 @@ impl LspStore {
                 }]))),
                 0,
                 false,
+                all_commit_ranges,
                 cx,
             )
         });

crates/proto/proto/lsp.proto πŸ”—

@@ -230,6 +230,7 @@ message ApplyCompletionAdditionalEdits {
   uint64 project_id = 1;
   uint64 buffer_id = 2;
   Completion completion = 3;
+  repeated AnchorRange all_commit_ranges = 4;
 }
 
 message ApplyCompletionAdditionalEditsResponse {

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

@@ -3,9 +3,9 @@ use collections::{HashMap, HashSet};
 use editor::{CompletionProvider, SelectionEffects};
 use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
 use gpui::{
-    App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, MouseButton,
-    PromptLevel, Subscription, Task, TextStyle, Tiling, TitlebarOptions, WindowBounds,
-    WindowHandle, WindowOptions, actions, point, size, transparent_black,
+    App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel,
+    Subscription, Task, TextStyle, Tiling, TitlebarOptions, WindowBounds, WindowHandle,
+    WindowOptions, actions, point, size, transparent_black,
 };
 use language::{Buffer, LanguageRegistry, language_settings::SoftWrap};
 use language_model::{
@@ -133,7 +133,6 @@ pub fn open_rules_library(
                     window_decorations: Some(window_decorations),
                     window_min_size: Some(DEFAULT_ADDITIONAL_WINDOW_SIZE),
                     kind: gpui::WindowKind::Floating,
-                    is_movable: !cfg!(target_os = "macos"),
                     ..Default::default()
                 },
                 |window, cx| {
@@ -504,7 +503,11 @@ impl RulesLibrary {
         });
 
         Self {
-            title_bar: Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx))),
+            title_bar: if !cfg!(target_os = "macos") {
+                Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx)))
+            } else {
+                None
+            },
             store,
             language_registry,
             rule_editors: HashMap::default(),
@@ -1126,44 +1129,30 @@ impl RulesLibrary {
         v_flex()
             .id("rule-list")
             .capture_action(cx.listener(Self::focus_active_rule))
+            .px_1p5()
             .h_full()
             .w_64()
             .overflow_x_hidden()
             .bg(cx.theme().colors().panel_background)
-            .when(!cfg!(target_os = "macos"), |this| this.px_1p5())
             .map(|this| {
                 if cfg!(target_os = "macos") {
-                    let Some(title_bar) = self.title_bar.as_ref() else {
-                        return this;
-                    };
-                    let button_padding = DynamicSpacing::Base08.rems(cx);
-                    let panel_background = cx.theme().colors().panel_background;
-                    title_bar.update(cx, |title_bar, _cx| {
-                        title_bar.set_background_color(Some(panel_background));
-                        title_bar.set_children(Some(
-                            h_flex()
-                                .w_full()
-                                .pr(button_padding)
-                                .justify_end()
-                                .child(
-                                    div()
-                                        .on_mouse_down(MouseButton::Left, |_, _, cx| {
-                                            cx.stop_propagation();
-                                        })
-                                        .child(
-                                            IconButton::new("new-rule", IconName::Plus)
-                                                .tooltip(move |_window, cx| {
-                                                    Tooltip::for_action("New Rule", &NewRule, cx)
-                                                })
-                                                .on_click(|_, window, cx| {
-                                                    window.dispatch_action(Box::new(NewRule), cx);
-                                                }),
-                                        ),
-                                )
-                                .into_any_element(),
-                        ));
-                    });
-                    this.child(title_bar.clone())
+                    this.child(
+                        h_flex()
+                            .p(DynamicSpacing::Base04.rems(cx))
+                            .h_9()
+                            .w_full()
+                            .flex_none()
+                            .justify_end()
+                            .child(
+                                IconButton::new("new-rule", IconName::Plus)
+                                    .tooltip(move |_window, cx| {
+                                        Tooltip::for_action("New Rule", &NewRule, cx)
+                                    })
+                                    .on_click(|_, window, cx| {
+                                        window.dispatch_action(Box::new(NewRule), cx);
+                                    }),
+                            ),
+                    )
                 } else {
                     this.child(
                         h_flex().p_1().w_full().child(
@@ -1182,12 +1171,7 @@ impl RulesLibrary {
                     )
                 }
             })
-            .child(
-                div()
-                    .flex_grow()
-                    .when(cfg!(target_os = "macos"), |this| this.px_1p5())
-                    .child(self.picker.clone()),
-            )
+            .child(div().flex_grow().child(self.picker.clone()))
     }
 
     fn render_active_rule_editor(
@@ -1434,9 +1418,7 @@ impl Render for RulesLibrary {
                 .overflow_hidden()
                 .font(ui_font)
                 .text_color(theme.colors().text)
-                .when(!cfg!(target_os = "macos"), |this| {
-                    this.children(self.title_bar.clone())
-                })
+                .children(self.title_bar.clone())
                 .bg(theme.colors().background)
                 .child(
                     h_flex()

crates/settings_content/src/agent.rs πŸ”—

@@ -11,7 +11,18 @@ use crate::DockPosition;
 
 /// Where new threads should start by default.
 #[derive(
-    Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom,
+    Clone,
+    Copy,
+    Debug,
+    Default,
+    PartialEq,
+    Eq,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+    MergeFrom,
+    strum::VariantArray,
+    strum::VariantNames,
 )]
 #[serde(rename_all = "snake_case")]
 pub enum NewThreadLocation {

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

@@ -593,6 +593,17 @@ pub struct GitPanelSettingsContent {
     ///
     /// Default: icon
     pub status_style: Option<StatusStyle>,
+
+    /// Whether to show file icons in the git panel.
+    ///
+    /// Default: false
+    pub file_icons: Option<bool>,
+
+    /// Whether to show folder icons or chevrons for directories in the git panel.
+    ///
+    /// Default: true
+    pub folder_icons: Option<bool>,
+
     /// How and when the scrollbar should be displayed.
     ///
     /// Default: inherits editor scrollbar settings

crates/settings_ui/Cargo.toml πŸ”—

@@ -28,6 +28,7 @@ cpal.workspace = true
 edit_prediction.workspace = true
 edit_prediction_ui.workspace = true
 editor.workspace = true
+feature_flags.workspace = true
 fs.workspace = true
 futures.workspace = true
 fuzzy.workspace = true

crates/settings_ui/src/page_data.rs πŸ”—

@@ -1,3 +1,4 @@
+use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
 use gpui::{Action as _, App};
 use itertools::Itertools as _;
 use settings::{
@@ -74,7 +75,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
         terminal_page(),
         version_control_page(),
         collaboration_page(),
-        ai_page(),
+        ai_page(cx),
         network_page(),
     ]
 }
@@ -5047,7 +5048,7 @@ fn panels_page() -> SettingsPage {
         ]
     }
 
-    fn git_panel_section() -> [SettingsPageItem; 11] {
+    fn git_panel_section() -> [SettingsPageItem; 13] {
         [
             SettingsPageItem::SectionHeader("Git Panel"),
             SettingsPageItem::SettingItem(SettingItem {
@@ -5189,6 +5190,42 @@ fn panels_page() -> SettingsPage {
                 metadata: None,
                 files: USER,
             }),
+            SettingsPageItem::SettingItem(SettingItem {
+                title: "File Icons",
+                description: "Show file icons next to the Git status icon.",
+                field: Box::new(SettingField {
+                    json_path: Some("git_panel.file_icons"),
+                    pick: |settings_content| {
+                        settings_content.git_panel.as_ref()?.file_icons.as_ref()
+                    },
+                    write: |settings_content, value| {
+                        settings_content
+                            .git_panel
+                            .get_or_insert_default()
+                            .file_icons = value;
+                    },
+                }),
+                metadata: None,
+                files: USER,
+            }),
+            SettingsPageItem::SettingItem(SettingItem {
+                title: "Folder Icons",
+                description: "Whether to show folder icons or chevrons for directories in the git panel.",
+                field: Box::new(SettingField {
+                    json_path: Some("git_panel.folder_icons"),
+                    pick: |settings_content| {
+                        settings_content.git_panel.as_ref()?.folder_icons.as_ref()
+                    },
+                    write: |settings_content, value| {
+                        settings_content
+                            .git_panel
+                            .get_or_insert_default()
+                            .folder_icons = value;
+                    },
+                }),
+                metadata: None,
+                files: USER,
+            }),
             SettingsPageItem::SettingItem(SettingItem {
                 title: "Diff Stats",
                 description: "Whether to show the addition/deletion change count next to each file in the Git panel.",
@@ -6978,7 +7015,7 @@ fn collaboration_page() -> SettingsPage {
     }
 }
 
-fn ai_page() -> SettingsPage {
+fn ai_page(cx: &App) -> SettingsPage {
     fn general_section() -> [SettingsPageItem; 2] {
         [
             SettingsPageItem::SectionHeader("General"),
@@ -6998,8 +7035,8 @@ fn ai_page() -> SettingsPage {
         ]
     }
 
-    fn agent_configuration_section() -> [SettingsPageItem; 13] {
-        [
+    fn agent_configuration_section(cx: &App) -> Box<[SettingsPageItem]> {
+        let mut items = vec![
             SettingsPageItem::SectionHeader("Agent Configuration"),
             SettingsPageItem::SubPageLink(SubPageLink {
                 title: "Tool Permissions".into(),
@@ -7010,11 +7047,14 @@ fn ai_page() -> SettingsPage {
                 files: USER,
                 render: render_tool_permissions_setup_page,
             }),
-            SettingsPageItem::SettingItem(SettingItem {
+        ];
+
+        if cx.has_flag::<AgentV2FeatureFlag>() {
+            items.push(SettingsPageItem::SettingItem(SettingItem {
                 title: "New Thread Location",
                 description: "Whether to start a new thread in the current local project or in a new Git worktree.",
                 field: Box::new(SettingField {
-                    json_path: Some("agent.default_start_thread_in"),
+                    json_path: Some("agent.new_thread_location"),
                     pick: |settings_content| {
                         settings_content
                             .agent
@@ -7031,7 +7071,10 @@ fn ai_page() -> SettingsPage {
                 }),
                 metadata: None,
                 files: USER,
-            }),
+            }));
+        }
+
+        items.extend([
             SettingsPageItem::SettingItem(SettingItem {
                 title: "Single File Review",
                 description: "When enabled, agent edits will also be displayed in single-file buffers for review.",
@@ -7236,7 +7279,9 @@ fn ai_page() -> SettingsPage {
                 metadata: None,
                 files: USER,
             }),
-        ]
+        ]);
+
+        items.into_boxed_slice()
     }
 
     fn context_servers_section() -> [SettingsPageItem; 2] {
@@ -7321,7 +7366,7 @@ fn ai_page() -> SettingsPage {
         title: "AI",
         items: concat_sections![
             general_section(),
-            agent_configuration_section(),
+            agent_configuration_section(cx),
             context_servers_section(),
             edit_prediction_language_settings_section(),
             edit_prediction_display_sub_section()

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

@@ -530,7 +530,7 @@ fn init_renderers(cx: &mut App) {
         .add_basic_renderer::<settings::VimInsertModeCursorShape>(render_dropdown)
         .add_basic_renderer::<settings::SteppingGranularity>(render_dropdown)
         .add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
-        .add_basic_renderer::<settings::NotifyWhenAgentWaiting>(render_dropdown)
+        .add_basic_renderer::<settings::NewThreadLocation>(render_dropdown)
         .add_basic_renderer::<settings::ImageFileSizeUnit>(render_dropdown)
         .add_basic_renderer::<settings::StatusStyle>(render_dropdown)
         .add_basic_renderer::<settings::EncodingDisplayOptions>(render_dropdown)

crates/ui/src/components/ai/thread_item.rs πŸ”—

@@ -3,7 +3,8 @@ use crate::{
     IconDecorationKind, prelude::*,
 };
 
-use gpui::{AnyView, ClickEvent, Hsla, SharedString};
+use gpui::{Animation, AnimationExt, AnyView, ClickEvent, Hsla, SharedString, pulsating_between};
+use std::time::Duration;
 
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
 pub enum AgentThreadStatus {
@@ -23,6 +24,7 @@ pub struct ThreadItem {
     timestamp: SharedString,
     notified: bool,
     status: AgentThreadStatus,
+    generating_title: bool,
     selected: bool,
     focused: bool,
     hovered: bool,
@@ -48,6 +50,7 @@ impl ThreadItem {
             timestamp: "".into(),
             notified: false,
             status: AgentThreadStatus::default(),
+            generating_title: false,
             selected: false,
             focused: false,
             hovered: false,
@@ -89,6 +92,11 @@ impl ThreadItem {
         self
     }
 
+    pub fn generating_title(mut self, generating: bool) -> Self {
+        self.generating_title = generating;
+        self
+    }
+
     pub fn selected(mut self, selected: bool) -> Self {
         self.selected = selected;
         self
@@ -221,7 +229,18 @@ impl RenderOnce for ThreadItem {
 
         let title = self.title;
         let highlight_positions = self.highlight_positions;
-        let title_label = if highlight_positions.is_empty() {
+        let title_label = if self.generating_title {
+            Label::new("New Thread…")
+                .color(Color::Muted)
+                .with_animation(
+                    "generating-title",
+                    Animation::new(Duration::from_secs(2))
+                        .repeat()
+                        .with_easing(pulsating_between(0.4, 0.8)),
+                    |label, delta| label.alpha(delta),
+                )
+                .into_any_element()
+        } else if highlight_positions.is_empty() {
             Label::new(title).into_any_element()
         } else {
             HighlightedLabel::new(title, highlight_positions).into_any_element()
@@ -284,7 +303,19 @@ impl RenderOnce for ThreadItem {
                     )
                     .child(gradient_overlay)
                     .when(self.hovered, |this| {
-                        this.when_some(self.action_slot, |this, slot| this.child(slot))
+                        this.when_some(self.action_slot, |this, slot| {
+                            let overlay = GradientFade::new(
+                                base_bg,
+                                color.element_hover,
+                                color.element_active,
+                            )
+                            .width(px(64.0))
+                            .right(px(6.))
+                            .gradient_stop(0.75)
+                            .group_name("thread-item");
+
+                            this.child(h_flex().relative().child(overlay).child(slot))
+                        })
                     }),
             )
             .when_some(self.worktree, |this, worktree| {
@@ -337,7 +368,7 @@ impl RenderOnce for ThreadItem {
                         .when(has_diff_stats, |this| {
                             this.child(
                                 DiffStat::new(diff_stat_id, added_count, removed_count)
-                                    .tooltip("Unreviewed changes"),
+                                    .tooltip("Unreviewed Changes"),
                             )
                         })
                         .when(has_diff_stats && has_timestamp, |this| {

crates/ui_input/src/input_field.rs πŸ”—

@@ -3,6 +3,7 @@ use component::{example_group, single_example};
 use gpui::{App, FocusHandle, Focusable, Hsla, Length};
 use std::sync::Arc;
 
+use ui::Tooltip;
 use ui::prelude::*;
 
 use crate::ErasedEditor;
@@ -38,6 +39,8 @@ pub struct InputField {
     tab_index: Option<isize>,
     /// Whether this field is a tab stop (can be focused via Tab key).
     tab_stop: bool,
+    /// Whether the field content is masked (for sensitive fields like passwords or API keys).
+    masked: Option<bool>,
 }
 
 impl Focusable for InputField {
@@ -63,6 +66,7 @@ impl InputField {
             min_width: px(192.).into(),
             tab_index: None,
             tab_stop: true,
+            masked: None,
         }
     }
 
@@ -96,6 +100,12 @@ impl InputField {
         self
     }
 
+    /// Sets this field as a masked/sensitive input (e.g., for passwords or API keys).
+    pub fn masked(mut self, masked: bool) -> Self {
+        self.masked = Some(masked);
+        self
+    }
+
     pub fn is_empty(&self, cx: &App) -> bool {
         self.editor().text(cx).trim().is_empty()
     }
@@ -115,12 +125,20 @@ impl InputField {
     pub fn set_text(&self, text: &str, window: &mut Window, cx: &mut App) {
         self.editor().set_text(text, window, cx)
     }
+
+    pub fn set_masked(&self, masked: bool, window: &mut Window, cx: &mut App) {
+        self.editor().set_masked(masked, window, cx)
+    }
 }
 
 impl Render for InputField {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let editor = self.editor.clone();
 
+        if let Some(masked) = self.masked {
+            self.editor.set_masked(masked, window, cx);
+        }
+
         let theme_color = cx.theme().colors();
 
         let style = InputFieldStyle {
@@ -172,7 +190,31 @@ impl Render for InputField {
                         this.gap_1()
                             .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
                     })
-                    .child(self.editor.render(window, cx)),
+                    .child(self.editor.render(window, cx))
+                    .when_some(self.masked, |this, is_masked| {
+                        this.child(
+                            IconButton::new(
+                                "toggle-masked",
+                                if is_masked {
+                                    IconName::Eye
+                                } else {
+                                    IconName::EyeOff
+                                },
+                            )
+                            .icon_size(IconSize::Small)
+                            .icon_color(Color::Muted)
+                            .tooltip(Tooltip::text(if is_masked { "Show" } else { "Hide" }))
+                            .on_click(cx.listener(
+                                |this, _, window, cx| {
+                                    if let Some(ref mut masked) = this.masked {
+                                        *masked = !*masked;
+                                        this.editor.set_masked(*masked, window, cx);
+                                        cx.notify();
+                                    }
+                                },
+                            )),
+                        )
+                    }),
             )
     }
 }

crates/util/Cargo.toml πŸ”—

@@ -21,7 +21,7 @@ test-support = ["git2", "rand", "util_macros"]
 anyhow.workspace = true
 async_zip.workspace = true
 collections.workspace = true
-dunce = "1.0"
+dunce.workspace = true
 futures-lite.workspace = true
 futures.workspace = true
 globset.workspace = true

crates/vim/src/helix.rs πŸ”—

@@ -12,6 +12,7 @@ use editor::{
 };
 use gpui::actions;
 use gpui::{Context, Window};
+use itertools::Itertools as _;
 use language::{CharClassifier, CharKind, Point};
 use search::{BufferSearchBar, SearchOptions};
 use settings::Settings;
@@ -876,11 +877,22 @@ impl Vim {
             self.update_editor(cx, |_vim, editor, cx| {
                 let snapshot = editor.snapshot(window, cx);
                 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
+                    let buffer = snapshot.buffer_snapshot();
+
                     s.select_anchor_ranges(
                         prior_selections
                             .iter()
                             .cloned()
-                            .chain(s.all_anchors(&snapshot).iter().map(|s| s.range())),
+                            .chain(s.all_anchors(&snapshot).iter().map(|s| s.range()))
+                            .sorted_by(|a, b| {
+                                a.start
+                                    .cmp(&b.start, buffer)
+                                    .then_with(|| a.end.cmp(&b.end, buffer))
+                            })
+                            .dedup_by(|a, b| {
+                                a.start.cmp(&b.start, buffer).is_eq()
+                                    && a.end.cmp(&b.end, buffer).is_eq()
+                            }),
                     );
                 })
             });
@@ -1670,6 +1682,25 @@ mod test {
         cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
     }
 
+    #[gpui::test]
+    async fn test_helix_select_next_match_wrapping(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        cx.enable_helix();
+
+        // Three occurrences of "one". After selecting all three with `n n`,
+        // pressing `n` again wraps the search to the first occurrence.
+        // The prior selections (at higher offsets) are chained before the
+        // wrapped selection (at a lower offset), producing unsorted anchors
+        // that cause `rope::Cursor::summary` to panic with
+        // "cannot summarize backward".
+        cx.set_state("Λ‡hello two one two one two one", Mode::HelixSelect);
+        cx.simulate_keystrokes("/ o n e");
+        cx.simulate_keystrokes("enter");
+        cx.simulate_keystrokes("n n n");
+        // Should not panic; all three occurrences should remain selected.
+        cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
+    }
+
     #[gpui::test]
     async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;

crates/vim/src/normal.rs πŸ”—

@@ -949,17 +949,16 @@ impl Vim {
             let current_line = point.row;
             let percentage = current_line as f32 / lines as f32;
             let modified = if buffer.is_dirty() { " [modified]" } else { "" };
-            vim.status_label = Some(
+            vim.set_status_label(
                 format!(
                     "{}{} {} lines --{:.0}%--",
                     filename,
                     modified,
                     lines,
                     percentage * 100.0,
-                )
-                .into(),
+                ),
+                cx,
             );
-            cx.notify();
         });
     }
 

crates/vim/src/normal/paste.rs πŸ”—

@@ -50,6 +50,10 @@ impl Vim {
                 })
                 .filter(|reg| !reg.text.is_empty())
                 else {
+                    vim.set_status_label(
+                        format!("Nothing in register {}", selected_register.unwrap_or('"')),
+                        cx,
+                    );
                     return;
                 };
                 let clipboard_selections = clipboard_selections
@@ -249,7 +253,7 @@ impl Vim {
     ) {
         self.stop_recording(cx);
         let selected_register = self.selected_register.take();
-        self.update_editor(cx, |_, editor, cx| {
+        self.update_editor(cx, |vim, editor, cx| {
             editor.transact(window, cx, |editor, window, cx| {
                 editor.set_clip_at_line_ends(false, cx);
                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
@@ -262,6 +266,10 @@ impl Vim {
                     globals.read_register(selected_register, Some(editor), cx)
                 })
                 .filter(|reg| !reg.text.is_empty()) else {
+                    vim.set_status_label(
+                        format!("Nothing in register {}", selected_register.unwrap_or('"')),
+                        cx,
+                    );
                     return;
                 };
                 editor.insert(&text, window, cx);
@@ -286,7 +294,7 @@ impl Vim {
     ) {
         self.stop_recording(cx);
         let selected_register = self.selected_register.take();
-        self.update_editor(cx, |_, editor, cx| {
+        self.update_editor(cx, |vim, editor, cx| {
             let text_layout_details = editor.text_layout_details(window, cx);
             editor.transact(window, cx, |editor, window, cx| {
                 editor.set_clip_at_line_ends(false, cx);
@@ -306,6 +314,10 @@ impl Vim {
                     globals.read_register(selected_register, Some(editor), cx)
                 })
                 .filter(|reg| !reg.text.is_empty()) else {
+                    vim.set_status_label(
+                        format!("Nothing in register {}", selected_register.unwrap_or('"')),
+                        cx,
+                    );
                     return;
                 };
                 editor.insert(&text, window, cx);

crates/vim/src/normal/repeat.rs πŸ”—

@@ -291,6 +291,24 @@ impl Vim {
         }) else {
             return;
         };
+
+        // Dot repeat always uses the recorded register, ignoring any "X
+        // override, as the register is an inherent part of the recorded action.
+        // For numbered registers, Neovim increments on each dot repeat so after
+        // using `"1p`, using `.` will equate to `"2p", the next `.` to `"3p`,
+        // etc..
+        let recorded_register = cx.global::<VimGlobals>().recorded_register_for_dot;
+        let next_register = recorded_register
+            .filter(|c| matches!(c, '1'..='9'))
+            .map(|c| ((c as u8 + 1).min(b'9')) as char);
+
+        self.selected_register = next_register.or(recorded_register);
+        if let Some(next_register) = next_register {
+            Vim::update_globals(cx, |globals, _| {
+                globals.recorded_register_for_dot = Some(next_register)
+            })
+        };
+
         if mode != Some(self.mode) {
             if let Some(mode) = mode {
                 self.switch_mode(mode, false, window, cx)
@@ -441,6 +459,207 @@ mod test {
         cx.shared_state().await.assert_eq("THE QUICK Λ‡brown fox");
     }
 
+    #[gpui::test]
+    async fn test_dot_repeat_registers_paste(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        // basic paste repeat uses the unnamed register
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes("y y p").await;
+        cx.shared_state().await.assert_eq("hello\nˇhello\n");
+        cx.simulate_shared_keystrokes(".").await;
+        cx.shared_state().await.assert_eq("hello\nhello\nˇhello\n");
+
+        // "_ (blackhole) is recorded and replayed, so the pasted text is still
+        // the original yanked line.
+        cx.set_shared_state(indoc! {"
+            Λ‡one
+            two
+            three
+            four
+        "})
+            .await;
+        cx.simulate_shared_keystrokes("y y j \" _ d d . p").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            one
+            four
+            Λ‡one
+        "});
+
+        // the recorded register is replayed, not whatever is in the unnamed register
+        cx.set_shared_state(indoc! {"
+            Λ‡one
+            two
+        "})
+            .await;
+        cx.simulate_shared_keystrokes("y y j \" a y y \" a p .")
+            .await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            one
+            two
+            two
+            Λ‡two
+        "});
+
+        // `"X.` ignores the override and always uses the recorded register.
+        // Both `dd` calls go into register `a`, so register `b` is empty and
+        // `"bp` pastes nothing.
+        cx.set_shared_state(indoc! {"
+            Λ‡one
+            two
+            three
+        "})
+            .await;
+        cx.simulate_shared_keystrokes("\" a d d \" b .").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            Λ‡three
+        "});
+        cx.simulate_shared_keystrokes("\" a p \" b p").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            three
+            Λ‡two
+        "});
+
+        // numbered registers cycle on each dot repeat: "1p . . uses registers 2, 3, …
+        // Since the cycling behavior caps at register 9, the first line to be
+        // deleted `1`, is no longer in any of the registers.
+        cx.set_shared_state(indoc! {"
+            Λ‡one
+            two
+            three
+            four
+            five
+            six
+            seven
+            eight
+            nine
+            ten
+        "})
+            .await;
+        cx.simulate_shared_keystrokes("d d . . . . . . . . .").await;
+        cx.shared_state().await.assert_eq(indoc! {"Λ‡"});
+        cx.simulate_shared_keystrokes("\" 1 p . . . . . . . . .")
+            .await;
+        cx.shared_state().await.assert_eq(indoc! {"
+
+            ten
+            nine
+            eight
+            seven
+            six
+            five
+            four
+            three
+            two
+            Λ‡two"});
+
+        // unnamed register repeat: dd records None, so . pastes the same
+        // deleted text
+        cx.set_shared_state(indoc! {"
+            Λ‡one
+            two
+            three
+        "})
+            .await;
+        cx.simulate_shared_keystrokes("d d p .").await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            two
+            one
+            Λ‡one
+            three
+        "});
+
+        // After `"1p` cycles to `2`, using `"ap` resets recorded_register to `a`,
+        // so the next `.` uses `a` and not 3.
+        cx.set_shared_state(indoc! {"
+            one
+            two
+            Λ‡three
+        "})
+            .await;
+        cx.simulate_shared_keystrokes("\" 2 y y k k \" a y y j \" 1 y y k \" 1 p . \" a p .")
+            .await;
+        cx.shared_state().await.assert_eq(indoc! {"
+            one
+            two
+            three
+            one
+            Λ‡one
+            two
+            three
+        "});
+    }
+
+    // This needs to be a separate test from `test_dot_repeat_registers_paste`
+    // as Neovim doesn't have support for using registers in replace operations
+    // by default.
+    #[gpui::test]
+    async fn test_dot_repeat_registers_replace(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.set_state(
+            indoc! {"
+            line Λ‡one
+            line two
+            line three
+        "},
+            Mode::Normal,
+        );
+
+        // 1. Yank `one` into register `a`
+        // 2. Move down and yank `two` into the default register
+        // 3. Replace `two` with the contents of register `a`
+        cx.simulate_keystrokes("\" a y w j y w \" a g R w");
+        cx.assert_state(
+            indoc! {"
+            line one
+            line onˇe
+            line three
+        "},
+            Mode::Normal,
+        );
+
+        // 1. Move down to `three`
+        // 2. Repeat the replace operation
+        cx.simulate_keystrokes("j .");
+        cx.assert_state(
+            indoc! {"
+            line one
+            line one
+            line onˇe
+        "},
+            Mode::Normal,
+        );
+
+        // Similar test, but this time using numbered registers, as those should
+        // automatically increase on successive uses of `.` .
+        cx.set_state(
+            indoc! {"
+            line Λ‡one
+            line two
+            line three
+            line four
+        "},
+            Mode::Normal,
+        );
+
+        // 1. Yank `one` into register `1`
+        // 2. Yank `two` into register `2`
+        // 3. Move down and yank `three` into the default register
+        // 4. Replace `three` with the contents of register `1`
+        // 5. Move down and repeat
+        cx.simulate_keystrokes("\" 1 y w j \" 2 y w j y w \" 1 g R w j .");
+        cx.assert_state(
+            indoc! {"
+            line one
+            line two
+            line one
+            line twˇo
+        "},
+            Mode::Normal,
+        );
+    }
+
     #[gpui::test]
     async fn test_repeat_ime(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;

crates/vim/src/state.rs πŸ”—

@@ -232,7 +232,15 @@ pub struct VimGlobals {
     pub recorded_actions: Vec<ReplayableAction>,
     pub recorded_selection: RecordedSelection,
 
+    /// The register being written to by the active `q{register}` macro
+    /// recording.
     pub recording_register: Option<char>,
+    /// The register that was selected at the start of the current
+    /// dot-recording, for example, `"ap`.
+    pub recording_register_for_dot: Option<char>,
+    /// The register from the last completed dot-recording. Used when replaying
+    /// with `.`.
+    pub recorded_register_for_dot: Option<char>,
     pub last_recorded_register: Option<char>,
     pub last_replayed_register: Option<char>,
     pub replayer: Option<Replayer>,
@@ -919,6 +927,7 @@ impl VimGlobals {
                 self.dot_recording = false;
                 self.recorded_actions = std::mem::take(&mut self.recording_actions);
                 self.recorded_count = self.recording_count.take();
+                self.recorded_register_for_dot = self.recording_register_for_dot.take();
                 self.stop_recording_after_next_action = false;
             }
         }
@@ -946,6 +955,7 @@ impl VimGlobals {
                 self.dot_recording = false;
                 self.recorded_actions = std::mem::take(&mut self.recording_actions);
                 self.recorded_count = self.recording_count.take();
+                self.recorded_register_for_dot = self.recording_register_for_dot.take();
                 self.stop_recording_after_next_action = false;
             }
         }

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

@@ -996,7 +996,14 @@ impl Vim {
         cx: &mut Context<Vim>,
         f: impl Fn(&mut Vim, &A, &mut Window, &mut Context<Vim>) + 'static,
     ) {
-        let subscription = editor.register_action(cx.listener(f));
+        let subscription = editor.register_action(cx.listener(move |vim, action, window, cx| {
+            if !Vim::globals(cx).dot_replaying {
+                if vim.status_label.take().is_some() {
+                    cx.notify();
+                }
+            }
+            f(vim, action, window, cx);
+        }));
         cx.on_release(|_, _| drop(subscription)).detach();
     }
 
@@ -1155,7 +1162,6 @@ impl Vim {
         let last_mode = self.mode;
         let prior_mode = self.last_mode;
         let prior_tx = self.current_tx;
-        self.status_label.take();
         self.last_mode = last_mode;
         self.mode = mode;
         self.operator_stack.clear();
@@ -1586,6 +1592,7 @@ impl Vim {
                 globals.dot_recording = true;
                 globals.recording_actions = Default::default();
                 globals.recording_count = None;
+                globals.recording_register_for_dot = self.selected_register;
 
                 let selections = self.editor().map(|editor| {
                     editor.update(cx, |editor, cx| {
@@ -2092,6 +2099,11 @@ impl Vim {
         editor.selections.set_line_mode(state.line_mode);
         editor.set_edit_predictions_hidden_for_vim_mode(state.hide_edit_predictions, window, cx);
     }
+
+    fn set_status_label(&mut self, label: impl Into<SharedString>, cx: &mut Context<Editor>) {
+        self.status_label = Some(label.into());
+        cx.notify();
+    }
 }
 
 struct VimEditorSettingsState {

crates/vim/test_data/test_dot_repeat_registers.json πŸ”—

@@ -0,0 +1,125 @@
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"p"}
+{"Get":{"state":"hello\nˇhello\n","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"hello\nhello\nˇhello\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡tocopytext\n1\n2\n3\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"_"}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"."}
+{"Key":"p"}
+{"Get":{"state":"tocopytext\n3\nˇtocopytext\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡tocopytext\n1\n2\n3\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"tocopytext\n1\n2\n3\nˇ1\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡one\ntwo\nthree\n"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"\""}
+{"Key":"b"}
+{"Key":"."}
+{"Get":{"state":"Λ‡three\n","mode":"Normal"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"\""}
+{"Key":"b"}
+{"Key":"p"}
+{"Get":{"state":"three\nˇtwo\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡line one\nline two\n"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"\""}
+{"Key":"b"}
+{"Key":"."}
+{"Get":{"state":"line one\nline two\nline one\nline one\nˇline one\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡1\n2\n3\n4\n5\n6\n7\n8\n9\n"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Get":{"state":"Λ‡","mode":"Normal"}}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Get":{"state":"\n9\n8\n7\n6\n5\n4\n3\n2\n1\nˇ1","mode":"Normal"}}
+{"Put":{"state":"Λ‡a\nb\nc\n"}}
+{"Key":"\""}
+{"Key":"9"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"\""}
+{"Key":"9"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"."}
+{"Get":{"state":"a\na\na\nˇa\nb\nc\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡one\ntwo\nthree\n"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"two\none\nˇone\nthree\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡one\ntwo\nthree\n"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"k"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"one\ntwo\n9\none\nˇone\ntwo\nthree\n","mode":"Normal"}}

crates/vim/test_data/test_dot_repeat_registers_paste.json πŸ”—

@@ -0,0 +1,105 @@
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"p"}
+{"Get":{"state":"hello\nˇhello\n","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"hello\nhello\nˇhello\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡one\ntwo\nthree\nfour\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"_"}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"."}
+{"Key":"p"}
+{"Get":{"state":"one\nfour\nˇone\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡one\ntwo\n"}}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"one\ntwo\ntwo\nˇtwo\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡one\ntwo\nthree\n"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"\""}
+{"Key":"b"}
+{"Key":"."}
+{"Get":{"state":"Λ‡three\n","mode":"Normal"}}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"\""}
+{"Key":"b"}
+{"Key":"p"}
+{"Get":{"state":"three\nˇtwo\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Get":{"state":"Λ‡","mode":"Normal"}}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Key":"."}
+{"Get":{"state":"\nten\nnine\neight\nseven\nsix\nfive\nfour\nthree\ntwo\nˇtwo","mode":"Normal"}}
+{"Put":{"state":"Λ‡one\ntwo\nthree\n"}}
+{"Key":"d"}
+{"Key":"d"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"two\none\nˇone\nthree\n","mode":"Normal"}}
+{"Put":{"state":"one\ntwo\nˇthree\n"}}
+{"Key":"\""}
+{"Key":"2"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"k"}
+{"Key":"k"}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"j"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"y"}
+{"Key":"y"}
+{"Key":"k"}
+{"Key":"\""}
+{"Key":"1"}
+{"Key":"p"}
+{"Key":"."}
+{"Key":"\""}
+{"Key":"a"}
+{"Key":"p"}
+{"Key":"."}
+{"Get":{"state":"one\ntwo\nthree\none\nˇone\ntwo\nthree\n","mode":"Normal"}}

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

@@ -61,12 +61,8 @@ pub fn init(cx: &mut App) {
 pub static FILTERED_KEYSTROKES: LazyLock<Vec<Vec<Keystroke>>> = LazyLock::new(|| {
     [
         // Modifiers on normal vim commands
-        "g h",
         "g j",
         "g k",
-        "g l",
-        "g $",
-        "g ^",
         // Duplicate keys with "ctrl" held, e.g. "ctrl-w ctrl-a" is duplicate of "ctrl-w a"
         "ctrl-w ctrl-a",
         "ctrl-w ctrl-c",

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

@@ -342,7 +342,7 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowO
         focus: false,
         show: false,
         kind: WindowKind::Normal,
-        is_movable: !cfg!(target_os = "macos"),
+        is_movable: true,
         display_id: display.map(|display| display.id()),
         window_background: cx.theme().window_background_appearance(),
         app_id: Some(app_id.to_owned()),

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

@@ -204,7 +204,7 @@ pub fn prompt_input_contains_special_tokens(input: &ZetaPromptInput, format: Zet
         .any(|token| input.cursor_excerpt.contains(token))
 }
 
-pub fn format_zeta_prompt(input: &ZetaPromptInput, format: ZetaFormat) -> String {
+pub fn format_zeta_prompt(input: &ZetaPromptInput, format: ZetaFormat) -> Option<String> {
     format_prompt_with_budget_for_format(input, format, MAX_PROMPT_TOKENS)
 }
 
@@ -416,7 +416,7 @@ pub fn format_prompt_with_budget_for_format(
     input: &ZetaPromptInput,
     format: ZetaFormat,
     max_tokens: usize,
-) -> String {
+) -> Option<String> {
     let (context, editable_range, context_range, cursor_offset) =
         resolve_cursor_region(input, format);
     let path = &*input.cursor_path;
@@ -436,25 +436,24 @@ pub fn format_prompt_with_budget_for_format(
         input_related_files
     };
 
-    match format {
-        ZetaFormat::V0211SeedCoder | ZetaFormat::V0304SeedNoEdits => {
-            seed_coder::format_prompt_with_budget(
+    let prompt = match format {
+        ZetaFormat::V0211SeedCoder
+        | ZetaFormat::V0304SeedNoEdits
+        | ZetaFormat::V0306SeedMultiRegions => {
+            let mut cursor_section = String::new();
+            write_cursor_excerpt_section_for_format(
+                format,
+                &mut cursor_section,
                 path,
                 context,
                 &editable_range,
                 cursor_offset,
-                &input.events,
-                related_files,
-                max_tokens,
-            )
-        }
-        ZetaFormat::V0306SeedMultiRegions => {
-            let cursor_prefix =
-                build_v0306_cursor_prefix(path, context, &editable_range, cursor_offset);
+            );
+
             seed_coder::assemble_fim_prompt(
                 context,
                 &editable_range,
-                &cursor_prefix,
+                &cursor_section,
                 &input.events,
                 related_files,
                 max_tokens,
@@ -497,7 +496,12 @@ pub fn format_prompt_with_budget_for_format(
             prompt.push_str(&cursor_section);
             prompt
         }
+    };
+    let prompt_tokens = estimate_tokens(prompt.len());
+    if prompt_tokens > max_tokens {
+        return None;
     }
+    return Some(prompt);
 }
 
 pub fn filter_redundant_excerpts(
@@ -2253,21 +2257,21 @@ pub mod hashline {
                 Case {
                     name: "insert_before_first_and_after_line",
                     original: indoc! {"
-                    a
-                    b
-                "},
+                        a
+                        b
+                    "},
                     model_output: indoc! {"
-                    <|insert|>
-                    HEAD
-                    <|insert|>0:61
-                    MID
-                "},
+                        <|insert|>
+                        HEAD
+                        <|insert|>0:61
+                        MID
+                    "},
                     expected: indoc! {"
-                    HEAD
-                    a
-                    MID
-                    b
-                "},
+                        HEAD
+                        a
+                        MID
+                        b
+                    "},
                 },
             ];
 
@@ -2707,8 +2711,8 @@ pub mod seed_coder {
     ) -> String {
         let suffix_section = build_suffix_section(context, editable_range);
 
-        let suffix_tokens = estimate_tokens(suffix_section.len());
-        let cursor_prefix_tokens = estimate_tokens(cursor_prefix_section.len());
+        let suffix_tokens = estimate_tokens(suffix_section.len() + FIM_PREFIX.len());
+        let cursor_prefix_tokens = estimate_tokens(cursor_prefix_section.len() + FIM_MIDDLE.len());
         let budget_after_cursor = max_tokens.saturating_sub(suffix_tokens + cursor_prefix_tokens);
 
         let edit_history_section = super::format_edit_history_within_budget(
@@ -2718,8 +2722,9 @@ pub mod seed_coder {
             budget_after_cursor,
             max_edit_event_count_for_format(&ZetaFormat::V0211SeedCoder),
         );
-        let edit_history_tokens = estimate_tokens(edit_history_section.len());
-        let budget_after_edit_history = budget_after_cursor.saturating_sub(edit_history_tokens);
+        let edit_history_tokens = estimate_tokens(edit_history_section.len() + "\n".len());
+        let budget_after_edit_history =
+            budget_after_cursor.saturating_sub(edit_history_tokens + "\n".len());
 
         let related_files_section = super::format_related_files_within_budget(
             related_files,
@@ -2741,6 +2746,7 @@ pub mod seed_coder {
         }
         prompt.push_str(cursor_prefix_section);
         prompt.push_str(FIM_MIDDLE);
+
         prompt
     }
 
@@ -4087,7 +4093,7 @@ mod tests {
         }
     }
 
-    fn format_with_budget(input: &ZetaPromptInput, max_tokens: usize) -> String {
+    fn format_with_budget(input: &ZetaPromptInput, max_tokens: usize) -> Option<String> {
         format_prompt_with_budget_for_format(input, ZetaFormat::V0114180EditableRegion, max_tokens)
     }
 
@@ -4102,7 +4108,7 @@ mod tests {
         );
 
         assert_eq!(
-            format_with_budget(&input, 10000),
+            format_with_budget(&input, 10000).unwrap(),
             indoc! {r#"
                 <|file_sep|>related.rs
                 fn helper() {}
@@ -4121,6 +4127,7 @@ mod tests {
                 suffix
                 <|fim_middle|>updated
             "#}
+            .to_string()
         );
     }
 
@@ -4132,18 +4139,18 @@ mod tests {
             2,
             vec![make_event("a.rs", "-x\n+y\n")],
             vec![
-                make_related_file("r1.rs", "a\n"),
-                make_related_file("r2.rs", "b\n"),
+                make_related_file("r1.rs", "aaaaaaa\n"),
+                make_related_file("r2.rs", "bbbbbbb\n"),
             ],
         );
 
         assert_eq!(
-            format_with_budget(&input, 10000),
+            format_with_budget(&input, 10000).unwrap(),
             indoc! {r#"
                 <|file_sep|>r1.rs
-                a
+                aaaaaaa
                 <|file_sep|>r2.rs
-                b
+                bbbbbbb
                 <|file_sep|>edit history
                 --- a/a.rs
                 +++ b/a.rs
@@ -4156,15 +4163,18 @@ mod tests {
                 <|fim_suffix|>
                 <|fim_middle|>updated
             "#}
+            .to_string()
         );
 
         assert_eq!(
-            format_with_budget(&input, 50),
-            indoc! {r#"
-                <|file_sep|>r1.rs
-                a
-                <|file_sep|>r2.rs
-                b
+            format_with_budget(&input, 55),
+            Some(
+                indoc! {r#"
+                <|file_sep|>edit history
+                --- a/a.rs
+                +++ b/a.rs
+                -x
+                +y
                 <|file_sep|>test.rs
                 <|fim_prefix|>
                 <|fim_middle|>current
@@ -4172,6 +4182,8 @@ mod tests {
                 <|fim_suffix|>
                 <|fim_middle|>updated
             "#}
+                .to_string()
+            )
         );
     }
 
@@ -4207,7 +4219,7 @@ mod tests {
         );
 
         assert_eq!(
-            format_with_budget(&input, 10000),
+            format_with_budget(&input, 10000).unwrap(),
             indoc! {r#"
                 <|file_sep|>big.rs
                 first excerpt
@@ -4222,10 +4234,11 @@ mod tests {
                 <|fim_suffix|>
                 <|fim_middle|>updated
             "#}
+            .to_string()
         );
 
         assert_eq!(
-            format_with_budget(&input, 50),
+            format_with_budget(&input, 50).unwrap(),
             indoc! {r#"
                 <|file_sep|>big.rs
                 first excerpt
@@ -4237,6 +4250,7 @@ mod tests {
                 <|fim_suffix|>
                 <|fim_middle|>updated
             "#}
+            .to_string()
         );
     }
 
@@ -4275,7 +4289,7 @@ mod tests {
 
         // With large budget, both files included; rendered in stable lexicographic order.
         assert_eq!(
-            format_with_budget(&input, 10000),
+            format_with_budget(&input, 10000).unwrap(),
             indoc! {r#"
                 <|file_sep|>file_a.rs
                 low priority content
@@ -4288,6 +4302,7 @@ mod tests {
                 <|fim_suffix|>
                 <|fim_middle|>updated
             "#}
+            .to_string()
         );
 
         // With tight budget, only file_b (lower order) fits.
@@ -4295,7 +4310,7 @@ mod tests {
         // file_b header (7) + excerpt (7) = 14 tokens, which fits.
         // file_a would need another 14 tokens, which doesn't fit.
         assert_eq!(
-            format_with_budget(&input, 52),
+            format_with_budget(&input, 52).unwrap(),
             indoc! {r#"
                 <|file_sep|>file_b.rs
                 high priority content
@@ -4306,6 +4321,7 @@ mod tests {
                 <|fim_suffix|>
                 <|fim_middle|>updated
             "#}
+            .to_string()
         );
     }
 
@@ -4347,7 +4363,7 @@ mod tests {
 
         // With large budget, all three excerpts included.
         assert_eq!(
-            format_with_budget(&input, 10000),
+            format_with_budget(&input, 10000).unwrap(),
             indoc! {r#"
                 <|file_sep|>mod.rs
                 mod header
@@ -4362,11 +4378,12 @@ mod tests {
                 <|fim_suffix|>
                 <|fim_middle|>updated
             "#}
+            .to_string()
         );
 
         // With tight budget, only order<=1 excerpts included (header + important fn).
         assert_eq!(
-            format_with_budget(&input, 55),
+            format_with_budget(&input, 55).unwrap(),
             indoc! {r#"
                 <|file_sep|>mod.rs
                 mod header
@@ -4380,6 +4397,7 @@ mod tests {
                 <|fim_suffix|>
                 <|fim_middle|>updated
             "#}
+            .to_string()
         );
     }
 
@@ -4394,7 +4412,7 @@ mod tests {
         );
 
         assert_eq!(
-            format_with_budget(&input, 10000),
+            format_with_budget(&input, 10000).unwrap(),
             indoc! {r#"
                 <|file_sep|>edit history
                 --- a/old.rs
@@ -4410,10 +4428,11 @@ mod tests {
                 <|fim_suffix|>
                 <|fim_middle|>updated
             "#}
+            .to_string()
         );
 
         assert_eq!(
-            format_with_budget(&input, 55),
+            format_with_budget(&input, 60).unwrap(),
             indoc! {r#"
                 <|file_sep|>edit history
                 --- a/new.rs
@@ -4426,6 +4445,7 @@ mod tests {
                 <|fim_suffix|>
                 <|fim_middle|>updated
             "#}
+            .to_string()
         );
     }
 
@@ -4439,25 +4459,19 @@ mod tests {
             vec![make_related_file("related.rs", "helper\n")],
         );
 
-        assert_eq!(
-            format_with_budget(&input, 30),
-            indoc! {r#"
-                <|file_sep|>test.rs
-                <|fim_prefix|>
-                <|fim_middle|>current
-                fn <|user_cursor|>main() {}
-                <|fim_suffix|>
-                <|fim_middle|>updated
-            "#}
-        );
+        assert!(format_with_budget(&input, 30).is_none())
     }
 
+    #[track_caller]
     fn format_seed_coder(input: &ZetaPromptInput) -> String {
         format_prompt_with_budget_for_format(input, ZetaFormat::V0211SeedCoder, 10000)
+            .expect("seed coder prompt formatting should succeed")
     }
 
+    #[track_caller]
     fn format_seed_coder_with_budget(input: &ZetaPromptInput, max_tokens: usize) -> String {
         format_prompt_with_budget_for_format(input, ZetaFormat::V0211SeedCoder, max_tokens)
+            .expect("seed coder prompt formatting should succeed")
     }
 
     #[test]
@@ -4542,17 +4556,22 @@ mod tests {
                 <[fim-middle]>"#}
         );
 
-        // With tight budget, context is dropped but cursor section remains
         assert_eq!(
-            format_seed_coder_with_budget(&input, 30),
+            format_prompt_with_budget_for_format(&input, ZetaFormat::V0211SeedCoder, 24),
+            None
+        );
+
+        assert_eq!(
+            format_seed_coder_with_budget(&input, 40),
             indoc! {r#"
                 <[fim-suffix]>
                 <[fim-prefix]><filename>test.rs
                 <<<<<<< CURRENT
                 co<|user_cursor|>de
                 =======
-                <[fim-middle]>"#}
-        );
+                <[fim-middle]>"#
+            }
+        )
     }
 
     #[test]
@@ -4603,21 +4622,20 @@ mod tests {
                 <[fim-middle]>"#}
         );
 
-        // With tight budget, only high_prio included.
-        // Cursor sections cost 25 tokens, so budget 44 leaves 19 for related files.
-        // high_prio header (7) + excerpt (3) = 10, fits. low_prio would add 10 more = 20 > 19.
+        // With tight budget under the generic heuristic, context is dropped but the
+        // minimal cursor section still fits.
         assert_eq!(
-            format_seed_coder_with_budget(&input, 44),
-            indoc! {r#"
-                <[fim-suffix]>
-                <[fim-prefix]><filename>high_prio.rs
-                high prio
-
-                <filename>test.rs
-                <<<<<<< CURRENT
-                co<|user_cursor|>de
-                =======
-                <[fim-middle]>"#}
+            format_prompt_with_budget_for_format(&input, ZetaFormat::V0211SeedCoder, 44),
+            Some(
+                indoc! {r#"
+                    <[fim-suffix]>
+                    <[fim-prefix]><filename>test.rs
+                    <<<<<<< CURRENT
+                    co<|user_cursor|>de
+                    =======
+                    <[fim-middle]>"#}
+                .to_string()
+            )
         );
     }
 

docs/src/SUMMARY.md πŸ”—

@@ -183,6 +183,7 @@
 # Account & Privacy
 
 - [Authenticate](./authentication.md)
+- [Roles](./roles.md)
 - [Privacy and Security](./ai/privacy-and-security.md)
   - [Worktree Trust](./worktree-trust.md)
   - [AI Improvement](./ai/ai-improvement.md)

docs/src/ai/external-agents.md πŸ”—

@@ -9,6 +9,8 @@ Zed supports many external agents, including CLI-based ones, through the [Agent
 
 Zed supports [Gemini CLI](https://github.com/google-gemini/gemini-cli) (the reference ACP implementation), [Claude Agent](https://platform.claude.com/docs/en/agent-sdk/overview), [Codex](https://developers.openai.com/codex), [GitHub Copilot](https://github.com/github/copilot-language-server-release), and [additional agents](#add-more-agents) you can configure.
 
+For Zed's built-in agent and the full list of tools it can use natively, see [Agent Tools](./tools.md).
+
 > Note that Zed's interaction with external agents is strictly UI-based; the billing, legal, and terms arrangement is directly between you and the agent provider.
 > Zed does not charge for use of external agents, and our [zero-data retention agreements/privacy guarantees](./ai-improvement.md) are **_only_** applicable for Zed's hosted models.
 

docs/src/ai/plans-and-usage.md πŸ”—

@@ -7,9 +7,9 @@ description: Understand Zed's AI plans, token-based usage metering, spend limits
 
 ## Available Plans {#plans}
 
-For costs and more information on pricing, visit [Zed’s pricing page](https://zed.dev/pricing).
+For costs and more information on pricing, visit [Zed's pricing page](https://zed.dev/pricing).
 
-Zed works without AI features or a subscription. No [authentication](../authentication.md) required for the editor itself.
+Zed works without AI features or a subscription. No [authentication](../authentication.md) is required for the editor itself.
 
 ## Usage {#usage}
 
@@ -17,6 +17,8 @@ Usage of Zed's hosted models is measured on a token basis, converted to dollars
 
 Zed Pro comes with $5 of monthly dollar credit. A trial of Zed Pro includes $20 of credit, usable for 14 days. Monthly included credit resets on your monthly billing date.
 
+The [Zed Student plan](https://zed.dev/education) includes $10/month in token credits. The Student plan is available free for one year to verified university students.
+
 To view your current usage, you can visit your account at [dashboard.zed.dev/account](https://dashboard.zed.dev/account). Information from our metering and billing provider, Orb, is embedded on that page.
 
 ## Spend Limits {#usage-spend-limits}
@@ -25,7 +27,9 @@ At the top of [the Account page](https://dashboard.zed.dev/account), you'll find
 
 The default value for all Pro users is $10, for a total monthly spend with Zed of $20 ($10 for your Pro subscription, $10 in incremental token spend). This can be set to $0 to limit your spend with Zed to exactly $10/month. If you adjust this limit _higher_ than $10 and consume more than $10 of incremental token spend, you'll be billed via [threshold billing](./billing.md#threshold-billing).
 
-Once the spend limit is hit, we’ll stop any further usage until your token spend limit resets.
+Once the spend limit is hit, we'll stop any further usage until your token spend limit resets.
+
+> **Note:** Spend limits are a Zed Pro feature. Student plan users do not currently have the ability to configure spend limits; usage is capped at the $10/month included credit.
 
 ## Business Usage {#business-usage}
 

docs/src/ai/tools.md πŸ”—

@@ -19,10 +19,14 @@ Gets errors and warnings for either a specific file or the entire project, usefu
 When a path is provided, shows all diagnostics for that specific file.
 When no path is provided, shows a summary of error and warning counts for all files in the project.
 
+**Example:** After editing `src/parser.rs`, call `diagnostics` with that path to check for type errors immediately. After a larger refactor touching many files, call it without a path to see a project-wide count of errors before deciding what to fix next.
+
 ### `fetch`
 
 Fetches a URL and returns the content as Markdown. Useful for providing docs as context.
 
+**Example:** Fetching a library's changelog page to check whether a breaking API change was introduced in a recent version before writing integration code.
+
 ### `find_path`
 
 Quickly finds files by matching glob patterns (like "\*_/_.js"), returning matching file paths alphabetically.
@@ -31,6 +35,8 @@ Quickly finds files by matching glob patterns (like "\*_/_.js"), returning match
 
 Searches file contents across the project using regular expressions, preferred for finding symbols in code without knowing exact file paths.
 
+**Example:** To find every call site of a function before renaming it, search for `parse_config\(` β€” the regex matches the function name followed by an opening parenthesis, filtering out comments or variable names that happen to contain the string.
+
 ### `list_directory`
 
 Lists files and directories in a given path, providing an overview of filesystem contents.
@@ -55,6 +61,8 @@ Allows the Agent to work through problems, brainstorm ideas, or plan without exe
 
 Searches the web for information, providing results with snippets and links from relevant web pages, useful for accessing real-time information.
 
+**Example:** Looking up whether a known bug in a dependency has been patched in a recent release, or finding the current API signature for a third-party library when the local docs are out of date.
+
 ## Edit Tools
 
 ### `copy_path`
@@ -73,6 +81,8 @@ Deletes a file or directory (including contents recursively) at the specified pa
 
 Edits files by replacing specific text with new content.
 
+**Example:** Updating a function signature β€” the agent identifies the exact lines to replace and provides the updated version, leaving the surrounding code untouched. For widespread renames, it pairs this with `grep` to find every occurrence first.
+
 ### `move_path`
 
 Moves or renames a file or directory in the project, performing a rename if only the filename differs.
@@ -89,8 +99,12 @@ Saves files that have unsaved changes. Used when files need to be saved before f
 
 Executes shell commands and returns the combined output, creating a new shell process for each invocation.
 
+**Example:** After editing a Rust file, run `cargo test --package my_crate 2>&1 | tail -30` to confirm the changes don't break existing tests. Or run `git diff --stat` to review which files have been modified before wrapping up a task.
+
 ## Other Tools
 
 ### `spawn_agent`
 
-Spawns a subagent with its own context window to perform a delegated task. Each subagent has access to the same tools as the parent agent.
+Spawns a subagent with its own context window to perform a delegated task. Useful for running parallel investigations, completing self-contained tasks, or performing research where only the outcome matters. Each subagent has access to the same tools as the parent agent.
+
+**Example:** While refactoring the authentication module, spawn a subagent to investigate how session tokens are validated elsewhere in the codebase. The parent agent continues its work and reviews the subagent's findings when it completes β€” keeping both context windows focused on a single task.

docs/src/roles.md πŸ”—

@@ -0,0 +1,71 @@
+---
+title: Roles - Zed
+description: Understand Zed's organization roles and what each role can access, manage, and configure.
+---
+
+# Roles
+
+Every member of a Zed organization is assigned a role that determines
+what they can access and configure.
+
+## Role Types {#roles}
+
+Every member of an organization is assigned one of three roles:
+
+| Role       | Description                                            |
+| ---------- | ------------------------------------------------------ |
+| **Owner**  | Full control, including billing and ownership transfer |
+| **Admin**  | Full control, except billing                           |
+| **Member** | Standard access, no privileged actions                 |
+
+### Owner {#role-owner}
+
+An owner has full control over the organization, including:
+
+- Invite and remove members
+- Assign and change member roles
+- Manage billing, payment methods, and invoices
+- Configure data-sharing policies
+- Disable Zed's collaborative features
+- Control whether members can use Zed-hosted models and Zed's edit predictions
+- Transfer ownership to another member
+
+### Admin {#role-admin}
+
+Admins have the same capabilities as the Owner, except they cannot:
+
+- Access or modify billing settings
+- Transfer organization ownership
+
+This role is suited for team leads or managers who handle day-to-day
+member access without needing visibility into payment details.
+
+### Member {#role-member}
+
+Members have standard access to Zed. They cannot access billing or
+organization settings.
+
+## Managing User Roles {#managing-users}
+
+Owners and Admins can manage organization members from the Zed
+dashboard within the Members page.
+
+### Inviting Members {#inviting-members}
+
+1. On the Members page, select **+ Invite Member**.
+2. Enter the member's company email address and choose a role.
+3. The invitee receives an email with instructions to join. After
+   accepting, they authenticate via GitHub.
+
+### Changing a Member's Role {#changing-roles}
+
+1. On the Members page, find the member. You can filter by role or
+   search by name.
+2. Open the three-dot menu and select a new role.
+
+### Removing a Member {#removing-members}
+
+1. On the Members page, find the member.
+2. Select **Remove** and confirm.
+
+Removing a member removes their access to organization settings and any organization-managed features. They can continue using Zed on their own.

extensions/glsl/Cargo.toml πŸ”—

@@ -1,6 +1,6 @@
 [package]
 name = "zed_glsl"
-version = "0.2.0"
+version = "0.2.1"
 edition.workspace = true
 publish.workspace = true
 license = "Apache-2.0"

extensions/glsl/extension.toml πŸ”—

@@ -1,7 +1,7 @@
 id = "glsl"
 name = "GLSL"
 description = "GLSL support."
-version = "0.2.0"
+version = "0.2.1"
 schema_version = 1
 authors = ["Mikayla Maki <mikayla@zed.dev>"]
 repository = "https://github.com/zed-industries/zed"

extensions/glsl/languages/glsl/highlights.scm πŸ”—

@@ -1,108 +1,68 @@
-"break" @keyword
-
-"case" @keyword
-
-"const" @keyword
-
-"continue" @keyword
-
-"default" @keyword
-
-"do" @keyword
-
-"else" @keyword
-
-"enum" @keyword
-
-"extern" @keyword
-
-"for" @keyword
-
-"if" @keyword
-
-"inline" @keyword
-
-"return" @keyword
-
-"sizeof" @keyword
-
-"static" @keyword
-
-"struct" @keyword
-
-"switch" @keyword
-
-"typedef" @keyword
-
-"union" @keyword
-
-"volatile" @keyword
-
-"while" @keyword
-
-"#define" @keyword
-
-"#elif" @keyword
-
-"#else" @keyword
-
-"#endif" @keyword
-
-"#if" @keyword
-
-"#ifdef" @keyword
-
-"#ifndef" @keyword
-
-"#include" @keyword
-
-(preproc_directive) @keyword
-
-"--" @operator
-
-"-" @operator
-
-"-=" @operator
-
-"->" @operator
-
-"=" @operator
-
-"!=" @operator
-
-"*" @operator
-
-"&" @operator
-
-"&&" @operator
-
-"+" @operator
-
-"++" @operator
-
-"+=" @operator
-
-"<" @operator
-
-"==" @operator
-
-">" @operator
-
-"||" @operator
-
-"." @delimiter
-
-";" @delimiter
+[
+  "break"
+  "case"
+  "const"
+  "continue"
+  "default"
+  "do"
+  "else"
+  "enum"
+  "extern"
+  "for"
+  "if"
+  "inline"
+  "return"
+  "sizeof"
+  "static"
+  "struct"
+  "switch"
+  "typedef"
+  "union"
+  "volatile"
+  "while"
+  "#define"
+  "#elif"
+  "#else"
+  "#endif"
+  "#if"
+  "#ifdef"
+  "#ifndef"
+  "#include"
+  (preproc_directive)
+] @keyword
 
-(string_literal) @string
+[
+  "--"
+  "-"
+  "-="
+  "->"
+  "="
+  "!="
+  "*"
+  "&"
+  "&&"
+  "+"
+  "++"
+  "+="
+  "<"
+  "=="
+  ">"
+  "||"
+  "."
+  ";"
+] @operator
 
-(system_lib_string) @string
+[
+  (string_literal)
+  (system_lib_string)
+] @string
 
 (null) @constant
 
-(number_literal) @number
-
-(char_literal) @number
+[
+  (number_literal)
+  (char_literal)
+] @number
 
 (identifier) @variable
 
@@ -110,11 +70,11 @@
 
 (statement_identifier) @label
 
-(type_identifier) @type
-
-(primitive_type) @type
-
-(sized_type_specifier) @type
+[
+  (type_identifier)
+  (primitive_type)
+  (sized_type_specifier)
+] @type
 
 (call_expression
   function: (identifier) @function)

extensions/html/Cargo.toml πŸ”—

@@ -1,6 +1,6 @@
 [package]
 name = "zed_html"
-version = "0.3.0"
+version = "0.3.1"
 edition.workspace = true
 publish.workspace = true
 license = "Apache-2.0"

extensions/html/extension.toml πŸ”—

@@ -1,7 +1,7 @@
 id = "html"
 name = "HTML"
 description = "HTML support."
-version = "0.3.0"
+version = "0.3.1"
 schema_version = 1
 authors = ["Isaac Clayton <slightknack@gmail.com>"]
 repository = "https://github.com/zed-industries/zed"

extensions/html/src/html.rs πŸ”—

@@ -95,11 +95,8 @@ impl zed::Extension for HtmlExtension {
         server_id: &LanguageServerId,
         worktree: &zed::Worktree,
     ) -> Result<Option<zed::serde_json::Value>> {
-        let settings = LspSettings::for_worktree(server_id.as_ref(), worktree)
-            .ok()
-            .and_then(|lsp_settings| lsp_settings.settings)
-            .unwrap_or_default();
-        Ok(Some(settings))
+        LspSettings::for_worktree(server_id.as_ref(), worktree)
+            .map(|lsp_settings| lsp_settings.settings)
     }
 
     fn language_server_initialization_options(

script/bundle-linux πŸ”—

@@ -74,7 +74,15 @@ fi
 export CC=${CC:-$(which clang)}
 
 # Build binary in release mode
-export RUSTFLAGS="${RUSTFLAGS:-} -C link-args=-Wl,--disable-new-dtags,-rpath,\$ORIGIN/../lib"
+# We need lld to link libwebrtc.a successfully on aarch64-linux.
+# NOTE: Since RUSTFLAGS env var overrides all .cargo/config.toml rustflags
+# (see https://github.com/rust-lang/cargo/issues/5376), the
+# [target.aarch64-unknown-linux-gnu] section in config.toml has no effect here.
+if [[ "$(uname -m)" == "aarch64" ]]; then
+    export RUSTFLAGS="${RUSTFLAGS:-} -C link-arg=-fuse-ld=lld -C link-args=-Wl,--disable-new-dtags,-rpath,\$ORIGIN/../lib"
+else
+    export RUSTFLAGS="${RUSTFLAGS:-} -C link-args=-Wl,--disable-new-dtags,-rpath,\$ORIGIN/../lib"
+fi
 cargo build --release --target "${target_triple}" --package zed --package cli
 # Build remote_server in separate invocation to prevent feature unification from other crates
 # from influencing dynamic libraries required by it.
@@ -111,10 +119,12 @@ else
     fi
 fi
 
-# Strip debug symbols and save them for upload to DigitalOcean
-objcopy --strip-debug "${target_dir}/${target_triple}/release/zed"
-objcopy --strip-debug "${target_dir}/${target_triple}/release/cli"
-objcopy --strip-debug "${target_dir}/${remote_server_triple}/release/remote_server"
+# Strip debug symbols and save them for upload to DigitalOcean.
+# We use llvm-objcopy because GNU objcopy on older distros (e.g. Ubuntu 20.04)
+# doesn't understand CREL sections produced by newer LLVM.
+llvm-objcopy --strip-debug "${target_dir}/${target_triple}/release/zed"
+llvm-objcopy --strip-debug "${target_dir}/${target_triple}/release/cli"
+llvm-objcopy --strip-debug "${target_dir}/${remote_server_triple}/release/remote_server"
 
 # Ensure that remote_server does not depend on libssl nor libcrypto, as we got rid of these deps.
 if ldd "${target_dir}/${remote_server_triple}/release/remote_server" | grep -q 'libcrypto\|libssl'; then

script/linux πŸ”—

@@ -39,6 +39,8 @@ if [[ -n $apt ]]; then
     make
     cmake
     clang
+    lld
+    llvm
     jq
     git
     curl

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

@@ -6,7 +6,7 @@ use crate::tasks::workflows::{
     runners,
     steps::{
         self, BASH_SHELL, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder,
-        NamedJob, checkout_repo, dependant_job, named,
+        NamedJob, cache_rust_dependencies_namespace, checkout_repo, dependant_job, named,
     },
     vars::{
         JobOutput, StepOutput, WorkflowInput, WorkflowSecret,
@@ -41,16 +41,17 @@ pub(crate) fn extension_bump() -> Workflow {
         &app_id,
         &app_secret,
     );
-    let create_label = create_version_label(
+    let (create_label, tag) = create_version_label(
         &dependencies,
         &version_changed,
         &current_version,
         &app_id,
         &app_secret,
     );
+    let tag = tag.as_job_output(&create_label);
     let trigger_release = trigger_release(
         &[&check_version_changed, &create_label],
-        current_version,
+        tag,
         &app_id,
         &app_secret,
     );
@@ -120,9 +121,10 @@ fn create_version_label(
     current_version: &JobOutput,
     app_id: &WorkflowSecret,
     app_secret: &WorkflowSecret,
-) -> NamedJob {
+) -> (NamedJob, StepOutput) {
     let (generate_token, generated_token) =
         generate_token(&app_id.to_string(), &app_secret.to_string(), None);
+    let (determine_tag_step, tag) = determine_tag(current_version);
     let job = steps::dependant_job(dependencies)
         .defaults(extension_job_defaults())
         .cond(Expression::new(format!(
@@ -130,16 +132,18 @@ fn create_version_label(
             github.ref == 'refs/heads/main' && {version_changed} == 'true'",
             version_changed = version_changed_output.expr(),
         )))
+        .outputs([(tag.name.to_owned(), tag.to_string())])
         .runs_on(runners::LINUX_SMALL)
         .timeout_minutes(1u32)
         .add_step(generate_token)
         .add_step(steps::checkout_repo())
-        .add_step(create_version_tag(current_version, generated_token));
+        .add_step(determine_tag_step)
+        .add_step(create_version_tag(&tag, generated_token));
 
-    named::job(job)
+    (named::job(job), tag)
 }
 
-fn create_version_tag(current_version: &JobOutput, generated_token: StepOutput) -> Step<Use> {
+fn create_version_tag(tag: &StepOutput, generated_token: StepOutput) -> Step<Use> {
     named::uses("actions", "github-script", "v7").with(
         Input::default()
             .add(
@@ -148,7 +152,7 @@ fn create_version_tag(current_version: &JobOutput, generated_token: StepOutput)
                     github.rest.git.createRef({{
                         owner: context.repo.owner,
                         repo: context.repo.repo,
-                        ref: 'refs/tags/v{current_version}',
+                        ref: 'refs/tags/{tag}',
                         sha: context.sha
                     }})"#
                 },
@@ -157,6 +161,26 @@ fn create_version_tag(current_version: &JobOutput, generated_token: StepOutput)
     )
 }
 
+fn determine_tag(current_version: &JobOutput) -> (Step<Run>, StepOutput) {
+    let step = named::bash(formatdoc! {r#"
+        EXTENSION_ID="$(sed -n 's/^id = "\(.*\)"/\1/p' < extension.toml | head -1 | tr -d '[:space:]')"
+
+        if [[ "$WORKING_DIR" == "." || -z "$WORKING_DIR" ]]; then
+            TAG="v${{CURRENT_VERSION}}"
+        else
+            TAG="${{EXTENSION_ID}}-v${{CURRENT_VERSION}}"
+        fi
+
+        echo "tag=${{TAG}}" >> "$GITHUB_OUTPUT"
+    "#})
+    .id("determine-tag")
+    .add_env(("CURRENT_VERSION", current_version.to_string()))
+    .add_env(("WORKING_DIR", "${{ inputs.working-directory }}"));
+
+    let tag = StepOutput::new(&step, "tag");
+    (step, tag)
+}
+
 /// Compares the current and previous commit and checks whether versions changed inbetween.
 pub(crate) fn compare_versions() -> (Step<Run>, StepOutput, StepOutput) {
     let check_needs_bump = named::bash(formatdoc! {
@@ -209,9 +233,10 @@ fn bump_extension_version(
             version_changed = version_changed_output.expr(),
         )))
         .runs_on(runners::LINUX_SMALL)
-        .timeout_minutes(3u32)
+        .timeout_minutes(5u32)
         .add_step(generate_token)
         .add_step(steps::checkout_repo())
+        .add_step(cache_rust_dependencies_namespace())
         .add_step(install_bump_2_version())
         .add_step(bump_version)
         .add_step(create_pull_request(
@@ -307,7 +332,13 @@ fn bump_version(
         else
             {{
                 echo "title=${{EXTENSION_ID}}: Bump to v${{NEW_VERSION}}";
-                echo "body=This PR bumps the version of the ${{EXTENSION_NAME}} extension to v${{NEW_VERSION}}";
+                echo "body<<EOF";
+                echo "This PR bumps the version of the ${{EXTENSION_NAME}} extension to v${{NEW_VERSION}}.";
+                echo "";
+                echo "Release Notes:";
+                echo "";
+                echo "- N/A";
+                echo "EOF";
                 echo "branch_name=zed-zippy-${{EXTENSION_ID}}-autobump";
             }} >> "$GITHUB_OUTPUT"
         fi
@@ -353,7 +384,7 @@ fn create_pull_request(
 
 fn trigger_release(
     dependencies: &[&NamedJob],
-    version: JobOutput,
+    tag: JobOutput,
     app_id: &WorkflowSecret,
     app_secret: &WorkflowSecret,
 ) -> NamedJob {
@@ -372,7 +403,7 @@ fn trigger_release(
         .add_step(generate_token)
         .add_step(checkout_repo())
         .add_step(get_extension_id)
-        .add_step(release_action(extension_id, version, generated_token));
+        .add_step(release_action(extension_id, tag, generated_token));
 
     named::job(job)
 }
@@ -393,14 +424,18 @@ fn get_extension_id() -> (Step<Run>, StepOutput) {
 
 fn release_action(
     extension_id: StepOutput,
-    version: JobOutput,
+    tag: JobOutput,
     generated_token: StepOutput,
 ) -> Step<Use> {
-    named::uses("huacnlee", "zed-extension-action", "v2")
-        .add_with(("extension-name", extension_id.to_string()))
-        .add_with(("push-to", "zed-industries/extensions"))
-        .add_with(("tag", format!("v{version}")))
-        .add_env(("COMMITTER_TOKEN", generated_token.to_string()))
+    named::uses(
+        "zed-extensions",
+        "update-action",
+        "543925fc45da8866b0d017218a656c8a3296ed3f",
+    )
+    .add_with(("extension-name", extension_id.to_string()))
+    .add_with(("push-to", "zed-industries/extensions"))
+    .add_with(("tag", tag.to_string()))
+    .add_env(("COMMITTER_TOKEN", generated_token.to_string()))
 }
 
 fn extension_workflow_secrets() -> (WorkflowSecret, WorkflowSecret) {

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

@@ -262,18 +262,12 @@ pub fn setup_linux() -> Step<Run> {
     named::bash("./script/linux")
 }
 
-fn install_mold() -> Step<Run> {
-    named::bash("./script/install-mold")
-}
-
 fn download_wasi_sdk() -> Step<Run> {
     named::bash("./script/download-wasi-sdk")
 }
 
 pub(crate) fn install_linux_dependencies(job: Job) -> Job {
-    job.add_step(setup_linux())
-        .add_step(install_mold())
-        .add_step(download_wasi_sdk())
+    job.add_step(setup_linux()).add_step(download_wasi_sdk())
 }
 
 pub fn script(name: &str) -> Step<Run> {