Merge branch 'main' into docs-reorganize

Katie Geer created

Change summary

.github/workflows/autofix_pr.yml                                          |  81 
.github/workflows/cherry_pick.yml                                         |   2 
.github/workflows/community_champion_auto_labeler.yml                     |   1 
.github/workflows/extension_tests.yml                                     |   3 
.github/workflows/release.yml                                             |  24 
.github/workflows/release_nightly.yml                                     |   6 
.github/workflows/run_tests.yml                                           |  51 
.mailmap                                                                  |   3 
Cargo.lock                                                                | 157 
Cargo.toml                                                                |  16 
Dockerfile-collab                                                         |   2 
REVIEWERS.conl                                                            |   8 
assets/keymaps/default-linux.json                                         |  15 
assets/keymaps/default-macos.json                                         |  18 
assets/keymaps/default-windows.json                                       |  12 
assets/prompts/content_prompt_v2.hbs                                      |   3 
assets/settings/default.json                                              |  22 
crates/acp_thread/src/acp_thread.rs                                       |   9 
crates/acp_thread/src/connection.rs                                       |  10 
crates/agent/src/agent.rs                                                 |   6 
crates/agent/src/history_store.rs                                         |  13 
crates/agent/src/tests/mod.rs                                             | 178 
crates/agent/src/thread.rs                                                |  28 
crates/agent/src/tools/context_server_registry.rs                         |  68 
crates/agent_settings/Cargo.toml                                          |   1 
crates/agent_settings/src/agent_settings.rs                               |  12 
crates/agent_ui/Cargo.toml                                                |   2 
crates/agent_ui/src/acp/message_editor.rs                                 | 249 
crates/agent_ui/src/acp/model_selector.rs                                 | 341 
crates/agent_ui/src/acp/model_selector_popover.rs                         |  58 
crates/agent_ui/src/acp/thread_view.rs                                    | 494 
crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs         |  10 
crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs |   2 
crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs          |  61 
crates/agent_ui/src/agent_diff.rs                                         |   8 
crates/agent_ui/src/agent_model_selector.rs                               |  37 
crates/agent_ui/src/agent_panel.rs                                        | 259 
crates/agent_ui/src/agent_ui.rs                                           |  14 
crates/agent_ui/src/buffer_codegen.rs                                     | 122 
crates/agent_ui/src/completion_provider.rs                                |   2 
crates/agent_ui/src/favorite_models.rs                                    |  57 
crates/agent_ui/src/inline_assistant.rs                                   |  34 
crates/agent_ui/src/inline_prompt_editor.rs                               |  88 
crates/agent_ui/src/language_model_selector.rs                            | 251 
crates/agent_ui/src/terminal_inline_assistant.rs                          |   6 
crates/agent_ui/src/text_thread_editor.rs                                 | 263 
crates/agent_ui/src/ui/acp_onboarding_modal.rs                            |   4 
crates/agent_ui/src/ui/claude_code_onboarding_modal.rs                    |   4 
crates/agent_ui/src/ui/model_selector_components.rs                       |  41 
crates/agent_ui/src/ui/onboarding_modal.rs                                |   4 
crates/agent_ui_v2/Cargo.toml                                             |   7 
crates/agent_ui_v2/LICENSE-GPL                                            |   2 
crates/anthropic/src/anthropic.rs                                         |  65 
crates/bedrock/src/bedrock.rs                                             |   2 
crates/buffer_diff/src/buffer_diff.rs                                     |  11 
crates/codestral/src/codestral.rs                                         |  12 
crates/collab/src/tests/remote_editing_collaboration_tests.rs             |   2 
crates/collab_ui/src/collab_panel.rs                                      |  16 
crates/collab_ui/src/collab_panel/channel_modal.rs                        |   2 
crates/command_palette/src/command_palette.rs                             |   6 
crates/context_server/Cargo.toml                                          |   1 
crates/context_server/src/client.rs                                       | 103 
crates/context_server/src/context_server.rs                               |  16 
crates/context_server/src/protocol.rs                                     |   6 
crates/copilot/src/copilot.rs                                             | 175 
crates/copilot/src/copilot_edit_prediction_delegate.rs                    | 421 
crates/copilot/src/request.rs                                             | 100 
crates/copilot/src/sign_in.rs                                             |   4 
crates/debugger_ui/src/debugger_panel.rs                                  |   4 
crates/debugger_ui/src/new_process_modal.rs                               |  22 
crates/debugger_ui/src/onboarding_modal.rs                                |   4 
crates/debugger_ui/src/session/running.rs                                 |   2 
crates/debugger_ui/src/session/running/breakpoint_list.rs                 |  10 
crates/debugger_ui/src/session/running/console.rs                         |   2 
crates/debugger_ui/src/session/running/memory_view.rs                     |   2 
crates/debugger_ui/src/session/running/variable_list.rs                   |   4 
crates/deepseek/src/deepseek.rs                                           |   5 
crates/diagnostics/src/buffer_diagnostics.rs                              |   6 
crates/diagnostics/src/diagnostic_renderer.rs                             |   2 
crates/diagnostics/src/diagnostics.rs                                     |   6 
crates/edit_prediction/src/mercury.rs                                     |  19 
crates/edit_prediction/src/onboarding_modal.rs                            |   4 
crates/edit_prediction/src/sweep_ai.rs                                    |  19 
crates/edit_prediction/src/zed_edit_prediction_delegate.rs                |  11 
crates/edit_prediction_cli/src/headless.rs                                |   2 
crates/edit_prediction_types/src/edit_prediction_types.rs                 |  26 
crates/edit_prediction_ui/src/rate_prediction_modal.rs                    |   2 
crates/editor/benches/editor_render.rs                                    |   6 
crates/editor/src/bracket_colorization.rs                                 |  55 
crates/editor/src/code_context_menus.rs                                   | 151 
crates/editor/src/display_map.rs                                          |  49 
crates/editor/src/display_map/block_map.rs                                |   3 
crates/editor/src/display_map/inlay_map.rs                                |   9 
crates/editor/src/display_map/wrap_map.rs                                 | 118 
crates/editor/src/edit_prediction_tests.rs                                |  18 
crates/editor/src/editor.rs                                               | 740 
crates/editor/src/editor_settings.rs                                      |   3 
crates/editor/src/editor_tests.rs                                         | 180 
crates/editor/src/element.rs                                              |  27 
crates/editor/src/git/blame.rs                                            |  82 
crates/editor/src/hover_links.rs                                          |   2 
crates/editor/src/mouse_context_menu.rs                                   |  10 
crates/editor/src/test.rs                                                 |   8 
crates/editor/src/test/editor_lsp_test_context.rs                         |   2 
crates/editor/src/test/editor_test_context.rs                             |   4 
crates/eval/src/eval.rs                                                   |   2 
crates/eval_utils/LICENSE-GPL                                             |   2 
crates/extension_api/src/extension_api.rs                                 |  13 
crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs                   |   2 
crates/file_finder/src/file_finder.rs                                     |   2 
crates/fs/src/fs.rs                                                       |  33 
crates/git/src/blame.rs                                                   |  11 
crates/git/src/commit.rs                                                  |  49 
crates/git_ui/src/blame_ui.rs                                             |   5 
crates/git_ui/src/branch_picker.rs                                        | 292 
crates/git_ui/src/commit_modal.rs                                         |  17 
crates/git_ui/src/commit_tooltip.rs                                       |   2 
crates/git_ui/src/file_history_view.rs                                    |   4 
crates/git_ui/src/git_panel.rs                                            | 647 
crates/git_ui/src/git_ui.rs                                               |   2 
crates/git_ui/src/onboarding.rs                                           |   4 
crates/git_ui/src/project_diff.rs                                         |  10 
crates/git_ui/src/worktree_picker.rs                                      |  41 
crates/go_to_line/src/go_to_line.rs                                       |   2 
crates/google_ai/src/google_ai.rs                                         |  24 
crates/gpui/Cargo.toml                                                    |   8 
crates/gpui/examples/focus_visible.rs                                     |  10 
crates/gpui/examples/input.rs                                             |   2 
crates/gpui/examples/on_window_close_quit.rs                              |   4 
crates/gpui/examples/tab_stop.rs                                          |  10 
crates/gpui/examples/window.rs                                            |  65 
crates/gpui/src/app.rs                                                    |  32 
crates/gpui/src/app/async_context.rs                                      |   2 
crates/gpui/src/app/context.rs                                            |   4 
crates/gpui/src/app/test_context.rs                                       |   2 
crates/gpui/src/elements/div.rs                                           |   6 
crates/gpui/src/elements/surface.rs                                       |   1 
crates/gpui/src/elements/text.rs                                          |  18 
crates/gpui/src/elements/uniform_list.rs                                  |   6 
crates/gpui/src/interactive.rs                                            |   4 
crates/gpui/src/key_dispatch.rs                                           | 229 
crates/gpui/src/keymap.rs                                                 |  35 
crates/gpui/src/platform.rs                                               |   4 
crates/gpui/src/platform/linux/wayland/client.rs                          |  79 
crates/gpui/src/platform/linux/wayland/window.rs                          | 106 
crates/gpui/src/platform/linux/x11/client.rs                              |  53 
crates/gpui/src/platform/linux/x11/window.rs                              |  88 
crates/gpui/src/platform/mac/metal_atlas.rs                               |   9 
crates/gpui/src/platform/mac/metal_renderer.rs                            |  78 
crates/gpui/src/platform/mac/open_type.rs                                 |   5 
crates/gpui/src/platform/mac/screen_capture.rs                            |  14 
crates/gpui/src/platform/mac/text_system.rs                               |  26 
crates/gpui/src/platform/mac/window.rs                                    |  52 
crates/gpui/src/platform/windows/events.rs                                |  13 
crates/gpui/src/platform/windows/platform.rs                              |   2 
crates/gpui/src/platform/windows/window.rs                                |  28 
crates/gpui/src/test.rs                                                   |   5 
crates/gpui/src/text_system/line_wrapper.rs                               |  59 
crates/gpui/src/window.rs                                                 |  47 
crates/gpui/src/window/prompts.rs                                         |   6 
crates/gpui_macros/src/derive_visual_context.rs                           |   2 
crates/keymap_editor/src/keymap_editor.rs                                 |  28 
crates/keymap_editor/src/ui_components/keystroke_input.rs                 |   4 
crates/language/Cargo.toml                                                |   2 
crates/language/src/buffer.rs                                             |  97 
crates/language/src/language.rs                                           |  23 
crates/language/src/syntax_map.rs                                         |  21 
crates/language/src/text_diff.rs                                          |  61 
crates/language_models/src/provider/anthropic.rs                          | 292 
crates/language_models/src/provider/bedrock.rs                            | 463 
crates/language_models/src/provider/cloud.rs                              |  10 
crates/language_models/src/provider/lmstudio.rs                           |   4 
crates/language_models/src/provider/ollama.rs                             |  10 
crates/language_tools/src/lsp_log_view.rs                                 |  16 
crates/language_tools/src/syntax_tree_view.rs                             |   2 
crates/languages/Cargo.toml                                               |   1 
crates/languages/src/css.rs                                               |  10 
crates/languages/src/json.rs                                              |  10 
crates/languages/src/python.rs                                            |  30 
crates/languages/src/rust.rs                                              |  40 
crates/languages/src/tailwind.rs                                          |  10 
crates/languages/src/typescript.rs                                        |  19 
crates/languages/src/vtsls.rs                                             |  76 
crates/languages/src/yaml.rs                                              |  11 
crates/lsp/src/lsp.rs                                                     |   4 
crates/markdown/src/markdown.rs                                           |  29 
crates/markdown_preview/src/markdown_preview_view.rs                      |   4 
crates/mistral/src/mistral.rs                                             |  10 
crates/multi_buffer/src/multi_buffer.rs                                   |   5 
crates/multi_buffer/src/multi_buffer_tests.rs                             |  13 
crates/node_runtime/src/node_runtime.rs                                   |  74 
crates/onboarding/src/onboarding.rs                                       |   6 
crates/outline/src/outline.rs                                             |   2 
crates/outline_panel/src/outline_panel.rs                                 |  12 
crates/outline_panel/src/outline_panel_settings.rs                        |   8 
crates/picker/src/picker.rs                                               |   2 
crates/project/Cargo.toml                                                 |   2 
crates/project/src/agent_server_store.rs                                  |  22 
crates/project/src/buffer_store.rs                                        |  10 
crates/project/src/context_server_store.rs                                |  19 
crates/project/src/debugger/session.rs                                    |  23 
crates/project/src/git_store.rs                                           |   5 
crates/project/src/lsp_store.rs                                           |  41 
crates/project/src/persistence.rs                                         | 355 
crates/project/src/prettier_store.rs                                      |   2 
crates/project/src/project.rs                                             |  42 
crates/project/src/project_settings.rs                                    |  23 
crates/project/src/trusted_worktrees.rs                                   | 669 
crates/project_benchmarks/src/main.rs                                     |   2 
crates/project_panel/src/project_panel.rs                                 |  18 
crates/project_panel/src/project_panel_settings.rs                        |   8 
crates/prompt_store/Cargo.toml                                            |   5 
crates/prompt_store/src/prompt_store.rs                                   | 281 
crates/prompt_store/src/prompts.rs                                        |  28 
crates/proto/proto/worktree.proto                                         |   6 
crates/recent_projects/src/remote_connections.rs                          |  14 
crates/recent_projects/src/remote_servers.rs                              |  32 
crates/remote/src/remote.rs                                               |   5 
crates/remote/src/remote_client.rs                                        |  61 
crates/remote/src/transport.rs                                            |  68 
crates/remote/src/transport/docker.rs                                     |  14 
crates/remote/src/transport/ssh.rs                                        | 353 
crates/remote/src/transport/wsl.rs                                        |   7 
crates/remote_server/src/headless_project.rs                              |   5 
crates/remote_server/src/unix.rs                                          |   7 
crates/rules_library/src/rules_library.rs                                 |  61 
crates/search/src/buffer_search.rs                                        |  31 
crates/search/src/project_search.rs                                       |  16 
crates/search/src/search.rs                                               |   2 
crates/search/src/search_bar.rs                                           |   2 
crates/settings/src/settings_content.rs                                   |  16 
crates/settings/src/settings_content/agent.rs                             |  13 
crates/settings/src/settings_content/language_model.rs                    |   2 
crates/settings/src/settings_content/project.rs                           |  24 
crates/settings/src/vscode_import.rs                                      |   1 
crates/settings_ui/src/page_data.rs                                       | 139 
crates/settings_ui/src/settings_ui.rs                                     |  64 
crates/supermaven/src/supermaven_edit_prediction_delegate.rs              |  11 
crates/terminal/src/terminal_hyperlinks.rs                                | 238 
crates/terminal_view/src/terminal_element.rs                              |   4 
crates/terminal_view/src/terminal_panel.rs                                | 135 
crates/terminal_view/src/terminal_view.rs                                 |  30 
crates/title_bar/src/title_bar.rs                                         |   2 
crates/toolchain_selector/src/toolchain_selector.rs                       |  10 
crates/ui/src/components.rs                                               |   2 
crates/ui/src/components/callout.rs                                       |   2 
crates/ui/src/components/context_menu.rs                                  |   4 
crates/ui/src/components/inline_code.rs                                   |  64 
crates/ui/src/components/navigable.rs                                     |   4 
crates/ui/src/components/popover_menu.rs                                  |  16 
crates/ui/src/components/right_click_menu.rs                              |  16 
crates/ui_input/src/number_field.rs                                       |   4 
crates/util/src/redact.rs                                                 |  34 
crates/vim/src/command.rs                                                 |   6 
crates/vim/src/motion.rs                                                  |   8 
crates/vim/src/object.rs                                                  |  12 
crates/which_key/Cargo.toml                                               |  23 
crates/which_key/LICENSE-GPL                                              |   1 
crates/which_key/src/which_key.rs                                         |  98 
crates/which_key/src/which_key_modal.rs                                   | 308 
crates/which_key/src/which_key_settings.rs                                |  18 
crates/workspace/src/dock.rs                                              |  22 
crates/workspace/src/item.rs                                              |  16 
crates/workspace/src/modal_layer.rs                                       |  26 
crates/workspace/src/pane.rs                                              |  10 
crates/workspace/src/persistence.rs                                       | 359 
crates/workspace/src/security_modal.rs                                    | 125 
crates/workspace/src/welcome.rs                                           |   4 
crates/workspace/src/workspace.rs                                         | 262 
crates/worktree/Cargo.toml                                                |   2 
crates/worktree/src/worktree.rs                                           | 104 
crates/worktree/src/worktree_tests.rs                                     | 191 
crates/zed/Cargo.toml                                                     |   5 
crates/zed/resources/Document.icns                                        |   0 
crates/zed/src/main.rs                                                    |  60 
crates/zed/src/zed.rs                                                     |  45 
crates/zed/src/zed/component_preview.rs                                   |   4 
crates/zed/src/zed/edit_prediction_registry.rs                            |  17 
crates/zed/src/zed/open_listener.rs                                       | 107 
crates/zed_actions/src/lib.rs                                             |   2 
crates/ztracing/src/lib.rs                                                |  22 
docs/src/completions.md                                                   |   4 
docs/src/development/glossary.md                                          |   2 
docs/src/migrate/_research-notes.md                                       |  73 
flake.lock                                                                |  30 
flake.nix                                                                 |   4 
nix/build.nix                                                             | 169 
rust-toolchain.toml                                                       |   2 
script/bundle-mac                                                         |  11 
script/danger/dangerfile.ts                                               |   3 
script/danger/package.json                                                |   2 
script/danger/pnpm-lock.yaml                                              |  10 
script/verify-macos-document-icon                                         |  81 
tooling/xtask/src/tasks/workflows/autofix_pr.rs                           | 124 
tooling/xtask/src/tasks/workflows/cherry_pick.rs                          |  17 
tooling/xtask/src/tasks/workflows/release.rs                              |   5 
tooling/xtask/src/tasks/workflows/run_tests.rs                            |  75 
tooling/xtask/src/tasks/workflows/steps.rs                                |  48 
298 files changed, 9,854 insertions(+), 5,154 deletions(-)

Detailed changes

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

@@ -9,26 +9,23 @@ on:
         description: pr_number
         required: true
         type: string
+      run_clippy:
+        description: run_clippy
+        type: boolean
+        default: 'true'
 jobs:
   run_autofix:
     runs-on: namespace-profile-16x32-ubuntu-2204
     steps:
-    - id: get-app-token
-      name: autofix_pr::run_autofix::authenticate_as_zippy
-      uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
-      with:
-        app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
-        private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
-    - name: steps::checkout_repo_with_token
+    - name: steps::checkout_repo
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
       with:
         clean: false
-        token: ${{ steps.get-app-token.outputs.token }}
     - name: autofix_pr::run_autofix::checkout_pr
       run: gh pr checkout ${{ inputs.pr_number }}
       shell: bash -euxo pipefail {0}
       env:
-        GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     - name: steps::setup_cargo_config
       run: |
         mkdir -p ./../.cargo
@@ -58,26 +55,74 @@ jobs:
       run: cargo fmt --all
       shell: bash -euxo pipefail {0}
     - name: autofix_pr::run_autofix::run_clippy_fix
+      if: ${{ inputs.run_clippy }}
       run: cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged
       shell: bash -euxo pipefail {0}
-    - name: autofix_pr::run_autofix::commit_and_push
+    - id: create-patch
+      name: autofix_pr::run_autofix::create_patch
       run: |
         if git diff --quiet; then
             echo "No changes to commit"
+            echo "has_changes=false" >> "$GITHUB_OUTPUT"
         else
-            git add -A
-            git commit -m "Autofix"
-            git push
+            git diff > autofix.patch
+            echo "has_changes=true" >> "$GITHUB_OUTPUT"
         fi
       shell: bash -euxo pipefail {0}
+    - name: upload artifact autofix-patch
+      uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
+      with:
+        name: autofix-patch
+        path: autofix.patch
+        if-no-files-found: ignore
+        retention-days: '1'
+    - name: steps::cleanup_cargo_config
+      if: always()
+      run: |
+        rm -rf ./../.cargo
+      shell: bash -euxo pipefail {0}
+    outputs:
+      has_changes: ${{ steps.create-patch.outputs.has_changes }}
+  commit_changes:
+    needs:
+    - run_autofix
+    if: needs.run_autofix.outputs.has_changes == 'true'
+    runs-on: namespace-profile-2x4-ubuntu-2404
+    steps:
+    - id: get-app-token
+      name: steps::authenticate_as_zippy
+      uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
+      with:
+        app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
+        private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
+    - name: steps::checkout_repo_with_token
+      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      with:
+        clean: false
+        token: ${{ steps.get-app-token.outputs.token }}
+    - name: autofix_pr::commit_changes::checkout_pr
+      run: gh pr checkout ${{ inputs.pr_number }}
+      shell: bash -euxo pipefail {0}
+      env:
+        GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
+    - name: autofix_pr::download_patch_artifact
+      uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
+      with:
+        name: autofix-patch
+    - name: autofix_pr::commit_changes::apply_patch
+      run: git apply autofix.patch
+      shell: bash -euxo pipefail {0}
+    - name: autofix_pr::commit_changes::commit_and_push
+      run: |
+        git commit -am "Autofix"
+        git push
+      shell: bash -euxo pipefail {0}
       env:
         GIT_COMMITTER_NAME: Zed Zippy
         GIT_COMMITTER_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
         GIT_AUTHOR_NAME: Zed Zippy
         GIT_AUTHOR_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
         GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
-    - name: steps::cleanup_cargo_config
-      if: always()
-      run: |
-        rm -rf ./../.cargo
-      shell: bash -euxo pipefail {0}
+concurrency:
+  group: ${{ github.workflow }}-${{ inputs.pr_number }}
+  cancel-in-progress: true

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

@@ -30,7 +30,7 @@ jobs:
       with:
         clean: false
     - id: get-app-token
-      name: cherry_pick::run_cherry_pick::authenticate_as_zippy
+      name: steps::authenticate_as_zippy
       uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
       with:
         app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}

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

@@ -61,7 +61,8 @@ jobs:
       uses: namespacelabs/nscloud-cache-action@v1
       with:
         cache: rust
-    - name: steps::cargo_fmt
+    - id: cargo_fmt
+      name: steps::cargo_fmt
       run: cargo fmt --all -- --check
       shell: bash -euxo pipefail {0}
     - name: extension_tests::run_clippy

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

@@ -26,7 +26,8 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy
       shell: bash -euxo pipefail {0}
     - name: steps::clear_target_dir_if_large
@@ -71,9 +72,15 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy
       shell: bash -euxo pipefail {0}
+    - id: record_clippy_failure
+      name: steps::record_clippy_failure
+      if: always()
+      run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
+      shell: bash -euxo pipefail {0}
     - name: steps::cargo_install_nextest
       uses: taiki-e/install-action@nextest
     - name: steps::clear_target_dir_if_large
@@ -87,6 +94,8 @@ jobs:
       run: |
         rm -rf ./../.cargo
       shell: bash -euxo pipefail {0}
+    outputs:
+      clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
     timeout-minutes: 60
   run_tests_windows:
     if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@@ -105,7 +114,8 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy.ps1
       shell: pwsh
     - name: steps::clear_target_dir_if_large
@@ -472,11 +482,17 @@ jobs:
     if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
     runs-on: namespace-profile-2x4-ubuntu-2404
     steps:
+    - id: get-app-token
+      name: steps::authenticate_as_zippy
+      uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
+      with:
+        app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
+        private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
     - name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
       run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
       shell: bash -euxo pipefail {0}
       env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
   notify_on_failure:
     needs:
     - upload_release_assets

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

@@ -20,7 +20,8 @@ jobs:
       with:
         clean: false
         fetch-depth: 0
-    - name: steps::cargo_fmt
+    - id: cargo_fmt
+      name: steps::cargo_fmt
       run: cargo fmt --all -- --check
       shell: bash -euxo pipefail {0}
     - name: ./script/clippy
@@ -44,7 +45,8 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy.ps1
       shell: pwsh
     - name: steps::clear_target_dir_if_large

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

@@ -74,9 +74,19 @@ jobs:
       uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
       with:
         version: '9'
-    - name: ./script/prettier
+    - id: prettier
+      name: steps::prettier
       run: ./script/prettier
       shell: bash -euxo pipefail {0}
+    - id: cargo_fmt
+      name: steps::cargo_fmt
+      run: cargo fmt --all -- --check
+      shell: bash -euxo pipefail {0}
+    - id: record_style_failure
+      name: steps::record_style_failure
+      if: always()
+      run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
+      shell: bash -euxo pipefail {0}
     - name: ./script/check-todos
       run: ./script/check-todos
       shell: bash -euxo pipefail {0}
@@ -87,9 +97,8 @@ jobs:
       uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
       with:
         config: ./typos.toml
-    - name: steps::cargo_fmt
-      run: cargo fmt --all -- --check
-      shell: bash -euxo pipefail {0}
+    outputs:
+      style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
     timeout-minutes: 60
   run_tests_windows:
     needs:
@@ -110,7 +119,8 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy.ps1
       shell: pwsh
     - name: steps::clear_target_dir_if_large
@@ -157,9 +167,15 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy
       shell: bash -euxo pipefail {0}
+    - id: record_clippy_failure
+      name: steps::record_clippy_failure
+      if: always()
+      run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
+      shell: bash -euxo pipefail {0}
     - name: steps::cargo_install_nextest
       uses: taiki-e/install-action@nextest
     - name: steps::clear_target_dir_if_large
@@ -173,6 +189,8 @@ jobs:
       run: |
         rm -rf ./../.cargo
       shell: bash -euxo pipefail {0}
+    outputs:
+      clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
     timeout-minutes: 60
   run_tests_mac:
     needs:
@@ -193,7 +211,8 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy
       shell: bash -euxo pipefail {0}
     - name: steps::clear_target_dir_if_large
@@ -573,6 +592,24 @@ jobs:
 
         exit $EXIT_CODE
       shell: bash -euxo pipefail {0}
+  call_autofix:
+    needs:
+    - check_style
+    - run_tests_linux
+    if: always() && (needs.check_style.outputs.style_failed == 'true' || needs.run_tests_linux.outputs.clippy_failed == 'true') && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
+    runs-on: namespace-profile-2x4-ubuntu-2404
+    steps:
+    - id: get-app-token
+      name: steps::authenticate_as_zippy
+      uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
+      with:
+        app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
+        private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
+    - name: run_tests::call_autofix::dispatch_autofix
+      run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=${{ needs.run_tests_linux.outputs.clippy_failed == 'true' }}
+      shell: bash -euxo pipefail {0}
+      env:
+        GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
   cancel-in-progress: true

.mailmap πŸ”—

@@ -141,6 +141,9 @@ Uladzislau Kaminski <i@uladkaminski.com>
 Uladzislau Kaminski <i@uladkaminski.com> <uladzislau_kaminski@epam.com>
 Vitaly Slobodin <vitaliy.slobodin@gmail.com>
 Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
+Yara <davidsk@zed.dev>
+Yara <git@davidsk.dev>
+Yara <git@yara.blue>
 Will Bradley <williambbradley@gmail.com>
 Will Bradley <williambbradley@gmail.com> <will@zed.dev>
 WindSoilder <WindSoilder@outlook.com>

Cargo.lock πŸ”—

@@ -226,9 +226,9 @@ dependencies = [
 
 [[package]]
 name = "agent-client-protocol"
-version = "0.9.0"
+version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
+checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
 dependencies = [
  "agent-client-protocol-schema",
  "anyhow",
@@ -243,9 +243,9 @@ dependencies = [
 
 [[package]]
 name = "agent-client-protocol-schema"
-version = "0.10.0"
+version = "0.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6"
+checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
 dependencies = [
  "anyhow",
  "derive_more 2.0.1",
@@ -301,6 +301,7 @@ dependencies = [
 name = "agent_settings"
 version = "0.1.0"
 dependencies = [
+ "agent-client-protocol",
  "anyhow",
  "cloud_llm_client",
  "collections",
@@ -792,7 +793,7 @@ dependencies = [
  "url",
  "wayland-backend",
  "wayland-client",
- "wayland-protocols 0.32.9",
+ "wayland-protocols",
  "zbus",
 ]
 
@@ -1440,9 +1441,9 @@ dependencies = [
 
 [[package]]
 name = "aws-config"
-version = "1.8.8"
+version = "1.8.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37cf2b6af2a95a20e266782b4f76f1a5e12bf412a9db2de9c1e9123b9d8c0ad8"
+checksum = "1856b1b48b65f71a4dd940b1c0931f9a7b646d4a924b9828ffefc1454714668a"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1506,9 +1507,9 @@ dependencies = [
 
 [[package]]
 name = "aws-runtime"
-version = "1.5.12"
+version = "1.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa006bb32360ed90ac51203feafb9d02e3d21046e1fd3a450a404b90ea73e5d"
+checksum = "9f2402da1a5e16868ba98725e5d73f26b8116eaa892e56f2cd0bf5eec7985f70"
 dependencies = [
  "aws-credential-types",
  "aws-sigv4",
@@ -1531,9 +1532,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-bedrockruntime"
-version = "1.109.0"
+version = "1.112.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbfdfd941dcb253c17bf70baddbf1e5b22f19e29d313d2e049bad4b1dadb2011"
+checksum = "c06c037e6823696d752702ec2bad758d3cf95d1b92b712c8ac7e93824b5e2391"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1613,9 +1614,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sso"
-version = "1.86.0"
+version = "1.88.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a0abbfab841446cce6e87af853a3ba2cc1bc9afcd3f3550dd556c43d434c86d"
+checksum = "d05b276777560aa9a196dbba2e3aada4d8006d3d7eeb3ba7fe0c317227d933c4"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1635,9 +1636,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-ssooidc"
-version = "1.88.0"
+version = "1.90.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a68d675582afea0e94d38b6ca9c5aaae4ca14f1d36faa6edb19b42e687e70d7"
+checksum = "f9be14d6d9cd761fac3fd234a0f47f7ed6c0df62d83c0eeb7012750e4732879b"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1657,9 +1658,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sts"
-version = "1.88.0"
+version = "1.90.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d30990923f4f675523c51eb1c0dec9b752fb267b36a61e83cbc219c9d86da715"
+checksum = "98a862d704c817d865c8740b62d8bbeb5adcb30965e93b471df8a5bcefa20a80"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1680,9 +1681,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sigv4"
-version = "1.3.5"
+version = "1.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bffc03068fbb9c8dd5ce1c6fb240678a5cffb86fb2b7b1985c999c4b83c8df68"
+checksum = "c35452ec3f001e1f2f6db107b6373f1f48f05ec63ba2c5c9fa91f07dad32af11"
 dependencies = [
  "aws-credential-types",
  "aws-smithy-eventstream",
@@ -1739,9 +1740,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-eventstream"
-version = "0.60.12"
+version = "0.60.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9656b85088f8d9dc7ad40f9a6c7228e1e8447cdf4b046c87e152e0805dea02fa"
+checksum = "e29a304f8319781a39808847efb39561351b1bb76e933da7aa90232673638658"
 dependencies = [
  "aws-smithy-types",
  "bytes 1.10.1",
@@ -1750,9 +1751,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-http"
-version = "0.62.4"
+version = "0.62.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3feafd437c763db26aa04e0cc7591185d0961e64c61885bece0fb9d50ceac671"
+checksum = "445d5d720c99eed0b4aa674ed00d835d9b1427dd73e04adaf2f94c6b2d6f9fca"
 dependencies = [
  "aws-smithy-eventstream",
  "aws-smithy-runtime-api",
@@ -1760,6 +1761,7 @@ dependencies = [
  "bytes 1.10.1",
  "bytes-utils",
  "futures-core",
+ "futures-util",
  "http 0.2.12",
  "http 1.3.1",
  "http-body 0.4.6",
@@ -1771,9 +1773,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-http-client"
-version = "1.1.3"
+version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1053b5e587e6fa40ce5a79ea27957b04ba660baa02b28b7436f64850152234f1"
+checksum = "623254723e8dfd535f566ee7b2381645f8981da086b5c4aa26c0c41582bb1d2c"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-runtime-api",
@@ -1801,9 +1803,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-json"
-version = "0.61.6"
+version = "0.61.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cff418fc8ec5cadf8173b10125f05c2e7e1d46771406187b2c878557d4503390"
+checksum = "2db31f727935fc63c6eeae8b37b438847639ec330a9161ece694efba257e0c54"
 dependencies = [
  "aws-smithy-types",
 ]
@@ -1829,9 +1831,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-runtime"
-version = "1.9.3"
+version = "1.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40ab99739082da5347660c556689256438defae3bcefd66c52b095905730e404"
+checksum = "0bbe9d018d646b96c7be063dd07987849862b0e6d07c778aad7d93d1be6c1ef0"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-http",
@@ -1853,9 +1855,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-runtime-api"
-version = "1.9.1"
+version = "1.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3683c5b152d2ad753607179ed71988e8cfd52964443b4f74fd8e552d0bbfeb46"
+checksum = "ec7204f9fd94749a7c53b26da1b961b4ac36bf070ef1e0b94bb09f79d4f6c193"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-types",
@@ -1870,9 +1872,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-types"
-version = "1.3.3"
+version = "1.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f5b3a7486f6690ba25952cabf1e7d75e34d69eaff5081904a47bc79074d6457"
+checksum = "25f535879a207fce0db74b679cfc3e91a3159c8144d717d55f5832aea9eef46e"
 dependencies = [
  "base64-simd",
  "bytes 1.10.1",
@@ -1896,18 +1898,18 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-xml"
-version = "0.60.11"
+version = "0.60.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9c34127e8c624bc2999f3b657e749c1393bedc9cd97b92a804db8ced4d2e163"
+checksum = "eab77cdd036b11056d2a30a7af7b775789fb024bf216acc13884c6c97752ae56"
 dependencies = [
  "xmlparser",
 ]
 
 [[package]]
 name = "aws-types"
-version = "1.3.9"
+version = "1.3.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2fd329bf0e901ff3f60425691410c69094dc2a1f34b331f37bfc4e9ac1565a1"
+checksum = "d79fb68e3d7fe5d4833ea34dc87d2e97d26d3086cb3da660bb6b1f76d98680b6"
 dependencies = [
  "aws-credential-types",
  "aws-smithy-async",
@@ -2665,9 +2667,9 @@ dependencies = [
 
 [[package]]
 name = "cap-fs-ext"
-version = "3.4.5"
+version = "3.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654"
+checksum = "e41cc18551193fe8fa6f15c1e3c799bc5ec9e2cfbfaa8ed46f37013e3e6c173c"
 dependencies = [
  "cap-primitives",
  "cap-std",
@@ -2677,9 +2679,9 @@ dependencies = [
 
 [[package]]
 name = "cap-net-ext"
-version = "3.4.5"
+version = "3.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7"
+checksum = "9f83833816c66c986e913b22ac887cec216ea09301802054316fc5301809702c"
 dependencies = [
  "cap-primitives",
  "cap-std",
@@ -2689,9 +2691,9 @@ dependencies = [
 
 [[package]]
 name = "cap-primitives"
-version = "3.4.5"
+version = "3.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a"
+checksum = "0a1e394ed14f39f8bc26f59d4c0c010dbe7f0a1b9bafff451b1f98b67c8af62a"
 dependencies = [
  "ambient-authority",
  "fs-set-times",
@@ -2707,9 +2709,9 @@ dependencies = [
 
 [[package]]
 name = "cap-rand"
-version = "3.4.5"
+version = "3.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d"
+checksum = "0acb89ccf798a28683f00089d0630dfaceec087234eae0d308c05ddeaa941b40"
 dependencies = [
  "ambient-authority",
  "rand 0.8.5",
@@ -2717,9 +2719,9 @@ dependencies = [
 
 [[package]]
 name = "cap-std"
-version = "3.4.5"
+version = "3.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a"
+checksum = "07c0355ca583dd58f176c3c12489d684163861ede3c9efa6fd8bba314c984189"
 dependencies = [
  "cap-primitives",
  "io-extras",
@@ -2729,9 +2731,9 @@ dependencies = [
 
 [[package]]
 name = "cap-time-ext"
-version = "3.4.5"
+version = "3.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80"
+checksum = "491af520b8770085daa0466978c75db90368c71896523f2464214e38359b1a5b"
 dependencies = [
  "ambient-authority",
  "cap-primitives",
@@ -2894,6 +2896,17 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "chardetng"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
+dependencies = [
+ "cfg-if",
+ "encoding_rs",
+ "memchr",
+]
+
 [[package]]
 name = "chrono"
 version = "0.4.42"
@@ -3622,6 +3635,7 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
+ "slotmap",
  "smol",
  "tempfile",
  "terminal",
@@ -7356,7 +7370,7 @@ dependencies = [
  "wayland-backend",
  "wayland-client",
  "wayland-cursor",
- "wayland-protocols 0.31.2",
+ "wayland-protocols",
  "wayland-protocols-plasma",
  "wayland-protocols-wlr",
  "windows 0.61.3",
@@ -8794,6 +8808,7 @@ dependencies = [
  "ctor",
  "diffy",
  "ec4rs",
+ "encoding_rs",
  "fs",
  "futures 0.3.31",
  "fuzzy",
@@ -8812,6 +8827,7 @@ dependencies = [
  "regex",
  "rpc",
  "schemars",
+ "semver",
  "serde",
  "serde_json",
  "settings",
@@ -9046,6 +9062,7 @@ dependencies = [
  "regex",
  "rope",
  "rust-embed",
+ "semver",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -12460,6 +12477,7 @@ dependencies = [
  "dap",
  "dap_adapters",
  "db",
+ "encoding_rs",
  "extension",
  "fancy-regex",
  "fs",
@@ -12630,6 +12648,8 @@ dependencies = [
  "paths",
  "rope",
  "serde",
+ "strum 0.27.2",
+ "tempfile",
  "text",
  "util",
  "uuid",
@@ -18909,18 +18929,6 @@ dependencies = [
  "xcursor",
 ]
 
-[[package]]
-name = "wayland-protocols"
-version = "0.31.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
-dependencies = [
- "bitflags 2.9.4",
- "wayland-backend",
- "wayland-client",
- "wayland-scanner",
-]
-
 [[package]]
 name = "wayland-protocols"
 version = "0.32.9"
@@ -18935,14 +18943,14 @@ dependencies = [
 
 [[package]]
 name = "wayland-protocols-plasma"
-version = "0.2.0"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
+checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
 dependencies = [
  "bitflags 2.9.4",
  "wayland-backend",
  "wayland-client",
- "wayland-protocols 0.31.2",
+ "wayland-protocols",
  "wayland-scanner",
 ]
 
@@ -18955,7 +18963,7 @@ dependencies = [
  "bitflags 2.9.4",
  "wayland-backend",
  "wayland-client",
- "wayland-protocols 0.32.9",
+ "wayland-protocols",
  "wayland-scanner",
 ]
 
@@ -19115,6 +19123,20 @@ dependencies = [
  "winsafe",
 ]
 
+[[package]]
+name = "which_key"
+version = "0.1.0"
+dependencies = [
+ "command_palette",
+ "gpui",
+ "serde",
+ "settings",
+ "theme",
+ "ui",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "whoami"
 version = "1.6.1"
@@ -20212,8 +20234,10 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-lock 2.8.0",
+ "chardetng",
  "clock",
  "collections",
+ "encoding_rs",
  "fs",
  "futures 0.3.31",
  "fuzzy",
@@ -20725,6 +20749,7 @@ dependencies = [
  "watch",
  "web_search",
  "web_search_providers",
+ "which_key",
  "windows 0.61.3",
  "winresource",
  "workspace",

Cargo.toml πŸ”—

@@ -192,6 +192,7 @@ members = [
     "crates/vercel",
     "crates/vim",
     "crates/vim_mode_setting",
+    "crates/which_key",
     "crates/watch",
     "crates/web_search",
     "crates/web_search_providers",
@@ -415,6 +416,7 @@ util_macros = { path = "crates/util_macros" }
 vercel = { path = "crates/vercel" }
 vim = { path = "crates/vim" }
 vim_mode_setting = { path = "crates/vim_mode_setting" }
+which_key = { path = "crates/which_key" }
 
 watch = { path = "crates/watch" }
 web_search = { path = "crates/web_search" }
@@ -436,7 +438,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
 # External crates
 #
 
-agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
+agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
 aho-corasick = "1.1"
 alacritty_terminal = "0.25.1-rc1"
 any_vec = "0.14"
@@ -455,15 +457,15 @@ async-task = "4.7"
 async-trait = "0.1"
 async-tungstenite = "0.31.0"
 async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] }
-aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
-aws-credential-types = { version = "1.2.2", features = [
+aws-config = { version = "1.8.10", features = ["behavior-version-latest"] }
+aws-credential-types = { version = "1.2.8", features = [
     "hardcoded-credentials",
 ] }
-aws-sdk-bedrockruntime = { version = "1.80.0", features = [
+aws-sdk-bedrockruntime = { version = "1.112.0", features = [
     "behavior-version-latest",
 ] }
-aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
-aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
+aws-smithy-runtime-api = { version = "1.9.2", features = ["http-1x", "client"] }
+aws-smithy-types = { version = "1.3.4", features = ["http-body-1-x"] }
 backtrace = "0.3"
 base64 = "0.22"
 bincode = "1.2.1"
@@ -476,6 +478,7 @@ bytes = "1.0"
 cargo_metadata = "0.19"
 cargo_toml = "0.21"
 cfg-if = "1.0.3"
+chardetng = "0.1"
 chrono = { version = "0.4", features = ["serde"] }
 ciborium = "0.2"
 circular-buffer = "1.0"
@@ -499,6 +502,7 @@ dotenvy = "0.15.0"
 ec4rs = "1.1"
 emojis = "0.6.1"
 env_logger = "0.11"
+encoding_rs = "0.8"
 exec = "0.3.1"
 fancy-regex = "0.16.0"
 fork = "0.4.0"

Dockerfile-collab πŸ”—

@@ -1,6 +1,6 @@
 # syntax = docker/dockerfile:1.2
 
-FROM rust:1.91.1-bookworm as builder
+FROM rust:1.92-bookworm as builder
 WORKDIR app
 COPY . .
 

REVIEWERS.conl πŸ”—

@@ -28,7 +28,7 @@ ai
   = @rtfeldman
 
 audio
-  = @dvdsk
+  = @yara-blue
 
 crashes
   = @p1n3appl3
@@ -53,7 +53,7 @@ extension
 git
   = @cole-miller
   = @danilo-leal
-  = @dvdsk
+  = @yara-blue
   = @kubkon
   = @Anthony-Eid
   = @cameron1024
@@ -76,7 +76,7 @@ languages
 
 linux
   = @cole-miller
-  = @dvdsk
+  = @yara-blue
   = @p1n3appl3
   = @probably-neb
   = @smitbarmase
@@ -92,7 +92,7 @@ multi_buffer
   = @SomeoneToIgnore
 
 pickers
-  = @dvdsk
+  = @yara-blue
   = @p1n3appl3
   = @SomeoneToIgnore
 

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

@@ -227,6 +227,7 @@
       "ctrl-g": "search::SelectNextMatch",
       "ctrl-shift-g": "search::SelectPreviousMatch",
       "ctrl-k l": "agent::OpenRulesLibrary",
+      "ctrl-shift-v": "agent::PasteRaw",
     },
   },
   {
@@ -252,6 +253,7 @@
       "ctrl-y": "agent::AllowOnce",
       "ctrl-alt-y": "agent::AllowAlways",
       "ctrl-alt-z": "agent::RejectOnce",
+      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -263,9 +265,9 @@
   {
     "context": "AgentPanel > Markdown",
     "bindings": {
-      "copy": "markdown::Copy",
-      "ctrl-insert": "markdown::Copy",
-      "ctrl-c": "markdown::Copy",
+      "copy": "markdown::CopyAsMarkdown",
+      "ctrl-insert": "markdown::CopyAsMarkdown",
+      "ctrl-c": "markdown::CopyAsMarkdown",
     },
   },
   {
@@ -292,6 +294,7 @@
       "shift-ctrl-r": "agent::OpenAgentDiff",
       "ctrl-shift-y": "agent::KeepAll",
       "ctrl-shift-n": "agent::RejectAll",
+      "ctrl-shift-v": "agent::PasteRaw",
     },
   },
   {
@@ -303,6 +306,7 @@
       "shift-ctrl-r": "agent::OpenAgentDiff",
       "ctrl-shift-y": "agent::KeepAll",
       "ctrl-shift-n": "agent::RejectAll",
+      "ctrl-shift-v": "agent::PasteRaw",
     },
   },
   {
@@ -346,6 +350,7 @@
       "ctrl-shift-y": "agent::KeepAll",
       "ctrl-shift-n": "agent::RejectAll",
       "shift-tab": "agent::CycleModeSelector",
+      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -903,8 +908,8 @@
     "bindings": {
       "left": "git_panel::CollapseSelectedEntry",
       "right": "git_panel::ExpandSelectedEntry",
-      "up": "menu::SelectPrevious",
-      "down": "menu::SelectNext",
+      "up": "git_panel::PreviousEntry",
+      "down": "git_panel::NextEntry",
       "enter": "menu::Confirm",
       "alt-y": "git::StageFile",
       "alt-shift-y": "git::UnstageFile",

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

@@ -266,6 +266,8 @@
       "cmd-g": "search::SelectNextMatch",
       "cmd-shift-g": "search::SelectPreviousMatch",
       "cmd-k l": "agent::OpenRulesLibrary",
+      "alt-tab": "agent::CycleFavoriteModels",
+      "cmd-shift-v": "agent::PasteRaw",
     },
   },
   {
@@ -292,6 +294,7 @@
       "cmd-y": "agent::AllowOnce",
       "cmd-alt-y": "agent::AllowAlways",
       "cmd-alt-z": "agent::RejectOnce",
+      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -304,7 +307,7 @@
     "context": "AgentPanel > Markdown",
     "use_key_equivalents": true,
     "bindings": {
-      "cmd-c": "markdown::Copy",
+      "cmd-c": "markdown::CopyAsMarkdown",
     },
   },
   {
@@ -333,6 +336,7 @@
       "shift-ctrl-r": "agent::OpenAgentDiff",
       "cmd-shift-y": "agent::KeepAll",
       "cmd-shift-n": "agent::RejectAll",
+      "cmd-shift-v": "agent::PasteRaw",
     },
   },
   {
@@ -345,6 +349,7 @@
       "shift-ctrl-r": "agent::OpenAgentDiff",
       "cmd-shift-y": "agent::KeepAll",
       "cmd-shift-n": "agent::RejectAll",
+      "cmd-shift-v": "agent::PasteRaw",
     },
   },
   {
@@ -386,6 +391,7 @@
       "cmd-shift-y": "agent::KeepAll",
       "cmd-shift-n": "agent::RejectAll",
       "shift-tab": "agent::CycleModeSelector",
+      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -397,6 +403,7 @@
       "cmd-shift-y": "agent::KeepAll",
       "cmd-shift-n": "agent::RejectAll",
       "shift-tab": "agent::CycleModeSelector",
+      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -880,6 +887,7 @@
     "use_key_equivalents": true,
     "bindings": {
       "cmd-alt-/": "agent::ToggleModelSelector",
+      "alt-tab": "agent::CycleFavoriteModels",
       "ctrl-[": "agent::CyclePreviousInlineAssist",
       "ctrl-]": "agent::CycleNextInlineAssist",
       "cmd-shift-enter": "inline_assistant::ThumbsUpResult",
@@ -976,12 +984,12 @@
     "context": "GitPanel && ChangesList",
     "use_key_equivalents": true,
     "bindings": {
+      "up": "git_panel::PreviousEntry",
+      "down": "git_panel::NextEntry",
+      "cmd-up": "git_panel::FirstEntry",
+      "cmd-down": "git_panel::LastEntry",
       "left": "git_panel::CollapseSelectedEntry",
       "right": "git_panel::ExpandSelectedEntry",
-      "up": "menu::SelectPrevious",
-      "down": "menu::SelectNext",
-      "cmd-up": "menu::SelectFirst",
-      "cmd-down": "menu::SelectLast",
       "enter": "menu::Confirm",
       "cmd-alt-y": "git::ToggleStaged",
       "space": "git::ToggleStaged",

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

@@ -227,6 +227,7 @@
       "ctrl-g": "search::SelectNextMatch",
       "ctrl-shift-g": "search::SelectPreviousMatch",
       "ctrl-k l": "agent::OpenRulesLibrary",
+      "ctrl-shift-v": "agent::PasteRaw",
     },
   },
   {
@@ -253,6 +254,7 @@
       "shift-alt-a": "agent::AllowOnce",
       "ctrl-alt-y": "agent::AllowAlways",
       "shift-alt-z": "agent::RejectOnce",
+      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -266,7 +268,7 @@
     "context": "AgentPanel > Markdown",
     "use_key_equivalents": true,
     "bindings": {
-      "ctrl-c": "markdown::Copy",
+      "ctrl-c": "markdown::CopyAsMarkdown",
     },
   },
   {
@@ -295,6 +297,7 @@
       "ctrl-shift-r": "agent::OpenAgentDiff",
       "ctrl-shift-y": "agent::KeepAll",
       "ctrl-shift-n": "agent::RejectAll",
+      "ctrl-shift-v": "agent::PasteRaw",
     },
   },
   {
@@ -307,6 +310,7 @@
       "ctrl-shift-r": "agent::OpenAgentDiff",
       "ctrl-shift-y": "agent::KeepAll",
       "ctrl-shift-n": "agent::RejectAll",
+      "ctrl-shift-v": "agent::PasteRaw",
     },
   },
   {
@@ -342,6 +346,7 @@
       "ctrl-shift-y": "agent::KeepAll",
       "ctrl-shift-n": "agent::RejectAll",
       "shift-tab": "agent::CycleModeSelector",
+      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -353,6 +358,7 @@
       "ctrl-shift-y": "agent::KeepAll",
       "ctrl-shift-n": "agent::RejectAll",
       "shift-tab": "agent::CycleModeSelector",
+      "alt-tab": "agent::CycleFavoriteModels",
     },
   },
   {
@@ -905,10 +911,10 @@
     "context": "GitPanel && ChangesList",
     "use_key_equivalents": true,
     "bindings": {
+      "up": "git_panel::PreviousEntry",
+      "down": "git_panel::NextEntry",
       "left": "git_panel::CollapseSelectedEntry",
       "right": "git_panel::ExpandSelectedEntry",
-      "up": "menu::SelectPrevious",
-      "down": "menu::SelectNext",
       "enter": "menu::Confirm",
       "alt-y": "git::StageFile",
       "shift-alt-y": "git::UnstageFile",

assets/prompts/content_prompt_v2.hbs πŸ”—

@@ -14,7 +14,6 @@ The section you'll need to rewrite is marked with <rewrite_this></rewrite_this>
 The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
 {{/if}}
 
-{{#if rewrite_section}}
 And here's the section to rewrite based on that prompt again for reference:
 
 <rewrite_this>
@@ -33,8 +32,6 @@ Below are the diagnostic errors visible to the user.  If the user requests probl
 {{/each}}
 {{/if}}
 
-{{/if}}
-
 Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
 
 Start at the indentation level in the original file in the rewritten {{content_type}}.

assets/settings/default.json πŸ”—

@@ -1321,6 +1321,14 @@
   "hidden_files": ["**/.*"],
   // Git gutter behavior configuration.
   "git": {
+    // Global switch to enable or disable all git integration features.
+    // If set to true, disables all git integration features.
+    // If set to false, individual git integration features below will be independently enabled or disabled.
+    "disable_git": false,
+    // Whether to enable git status tracking.
+    "enable_status": true,
+    // Whether to enable git diff display.
+    "enable_diff": true,
     // Control whether the git gutter is shown. May take 2 values:
     // 1. Show the gutter
     //      "git_gutter": "tracked_files"
@@ -1705,7 +1713,12 @@
   // }
   //
   "file_types": {
-    "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
+    "JSONC": [
+      "**/.zed/*.json",
+      "**/.vscode/**/*.json",
+      "**/{zed,Zed}/{settings,keymap,tasks,debug}.json",
+      "tsconfig*.json",
+    ],
     "Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
     "Shell Script": [".env.*"],
   },
@@ -2152,6 +2165,13 @@
     // The shape can be one of the following: "block", "bar", "underline", "hollow".
     "cursor_shape": {},
   },
+  // Which-key popup settings
+  "which_key": {
+    // Whether to show the which-key popup when holding down key combinations.
+    "enabled": false,
+    // Delay in milliseconds before showing the which-key popup.
+    "delay_ms": 1000,
+  },
   // The server to connect to. If the environment variable
   // ZED_SERVER_URL is set, it will override this setting.
   "server_url": "https://zed.dev",

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

@@ -192,6 +192,7 @@ pub struct ToolCall {
     pub locations: Vec<acp::ToolCallLocation>,
     pub resolved_locations: Vec<Option<AgentLocation>>,
     pub raw_input: Option<serde_json::Value>,
+    pub raw_input_markdown: Option<Entity<Markdown>>,
     pub raw_output: Option<serde_json::Value>,
 }
 
@@ -222,6 +223,11 @@ impl ToolCall {
             }
         }
 
+        let raw_input_markdown = tool_call
+            .raw_input
+            .as_ref()
+            .and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
+
         let result = Self {
             id: tool_call.tool_call_id,
             label: cx
@@ -232,6 +238,7 @@ impl ToolCall {
             resolved_locations: Vec::default(),
             status,
             raw_input: tool_call.raw_input,
+            raw_input_markdown,
             raw_output: tool_call.raw_output,
         };
         Ok(result)
@@ -307,6 +314,7 @@ impl ToolCall {
         }
 
         if let Some(raw_input) = raw_input {
+            self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
             self.raw_input = Some(raw_input);
         }
 
@@ -1355,6 +1363,7 @@ impl AcpThread {
                     locations: Vec::new(),
                     resolved_locations: Vec::new(),
                     raw_input: None,
+                    raw_input_markdown: None,
                     raw_output: None,
                 };
                 self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);

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

@@ -202,6 +202,12 @@ pub trait AgentModelSelector: 'static {
     fn should_render_footer(&self) -> bool {
         false
     }
+
+    /// Whether this selector supports the favorites feature.
+    /// Only the native agent uses the model ID format that maps to settings.
+    fn supports_favorites(&self) -> bool {
+        false
+    }
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -239,6 +245,10 @@ impl AgentModelList {
             AgentModelList::Grouped(groups) => groups.is_empty(),
         }
     }
+
+    pub fn is_flat(&self) -> bool {
+        matches!(self, AgentModelList::Flat(_))
+    }
 }
 
 #[cfg(feature = "test-support")]

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

@@ -426,7 +426,7 @@ impl NativeAgent {
                 .into_iter()
                 .flat_map(|(contents, prompt_metadata)| match contents {
                     Ok(contents) => Some(UserRulesContext {
-                        uuid: prompt_metadata.id.user_id()?,
+                        uuid: prompt_metadata.id.as_user()?,
                         title: prompt_metadata.title.map(|title| title.to_string()),
                         contents,
                     }),
@@ -1164,6 +1164,10 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
     fn should_render_footer(&self) -> bool {
         true
     }
+
+    fn supports_favorites(&self) -> bool {
+        true
+    }
 }
 
 impl acp_thread::AgentConnection for NativeAgentConnection {

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

@@ -216,14 +216,10 @@ impl HistoryStore {
     }
 
     pub fn reload(&self, cx: &mut Context<Self>) {
-        let database_future = ThreadsDatabase::connect(cx);
+        let database_connection = ThreadsDatabase::connect(cx);
         cx.spawn(async move |this, cx| {
-            let threads = database_future
-                .await
-                .map_err(|err| anyhow!(err))?
-                .list_threads()
-                .await?;
-
+            let database = database_connection.await;
+            let threads = database.map_err(|err| anyhow!(err))?.list_threads().await?;
             this.update(cx, |this, cx| {
                 if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES {
                     for thread in threads
@@ -344,7 +340,8 @@ impl HistoryStore {
     fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
         cx.background_spawn(async move {
             if cfg!(any(feature = "test-support", test)) {
-                anyhow::bail!("history store does not persist in tests");
+                log::warn!("history store does not persist in tests");
+                return Ok(VecDeque::new());
             }
             let json = KEY_VALUE_STORE
                 .read_kvp(RECENTLY_OPENED_THREADS_KEY)?

crates/agent/src/tests/mod.rs πŸ”—

@@ -2809,3 +2809,181 @@ fn setup_context_server(
     cx.run_until_parked();
     mcp_tool_calls_rx
 }
+
+#[gpui::test]
+async fn test_tokens_before_message(cx: &mut TestAppContext) {
+    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+    let fake_model = model.as_fake();
+
+    // First message
+    let message_1_id = UserMessageId::new();
+    thread
+        .update(cx, |thread, cx| {
+            thread.send(message_1_id.clone(), ["First message"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+
+    // Before any response, tokens_before_message should return None for first message
+    thread.read_with(cx, |thread, _| {
+        assert_eq!(
+            thread.tokens_before_message(&message_1_id),
+            None,
+            "First message should have no tokens before it"
+        );
+    });
+
+    // Complete first message with usage
+    fake_model.send_last_completion_stream_text_chunk("Response 1");
+    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
+        language_model::TokenUsage {
+            input_tokens: 100,
+            output_tokens: 50,
+            cache_creation_input_tokens: 0,
+            cache_read_input_tokens: 0,
+        },
+    ));
+    fake_model.end_last_completion_stream();
+    cx.run_until_parked();
+
+    // First message still has no tokens before it
+    thread.read_with(cx, |thread, _| {
+        assert_eq!(
+            thread.tokens_before_message(&message_1_id),
+            None,
+            "First message should still have no tokens before it after response"
+        );
+    });
+
+    // Second message
+    let message_2_id = UserMessageId::new();
+    thread
+        .update(cx, |thread, cx| {
+            thread.send(message_2_id.clone(), ["Second message"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+
+    // Second message should have first message's input tokens before it
+    thread.read_with(cx, |thread, _| {
+        assert_eq!(
+            thread.tokens_before_message(&message_2_id),
+            Some(100),
+            "Second message should have 100 tokens before it (from first request)"
+        );
+    });
+
+    // Complete second message
+    fake_model.send_last_completion_stream_text_chunk("Response 2");
+    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
+        language_model::TokenUsage {
+            input_tokens: 250, // Total for this request (includes previous context)
+            output_tokens: 75,
+            cache_creation_input_tokens: 0,
+            cache_read_input_tokens: 0,
+        },
+    ));
+    fake_model.end_last_completion_stream();
+    cx.run_until_parked();
+
+    // Third message
+    let message_3_id = UserMessageId::new();
+    thread
+        .update(cx, |thread, cx| {
+            thread.send(message_3_id.clone(), ["Third message"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+
+    // Third message should have second message's input tokens (250) before it
+    thread.read_with(cx, |thread, _| {
+        assert_eq!(
+            thread.tokens_before_message(&message_3_id),
+            Some(250),
+            "Third message should have 250 tokens before it (from second request)"
+        );
+        // Second message should still have 100
+        assert_eq!(
+            thread.tokens_before_message(&message_2_id),
+            Some(100),
+            "Second message should still have 100 tokens before it"
+        );
+        // First message still has none
+        assert_eq!(
+            thread.tokens_before_message(&message_1_id),
+            None,
+            "First message should still have no tokens before it"
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_tokens_before_message_after_truncate(cx: &mut TestAppContext) {
+    let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+    let fake_model = model.as_fake();
+
+    // Set up three messages with responses
+    let message_1_id = UserMessageId::new();
+    thread
+        .update(cx, |thread, cx| {
+            thread.send(message_1_id.clone(), ["Message 1"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+    fake_model.send_last_completion_stream_text_chunk("Response 1");
+    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
+        language_model::TokenUsage {
+            input_tokens: 100,
+            output_tokens: 50,
+            cache_creation_input_tokens: 0,
+            cache_read_input_tokens: 0,
+        },
+    ));
+    fake_model.end_last_completion_stream();
+    cx.run_until_parked();
+
+    let message_2_id = UserMessageId::new();
+    thread
+        .update(cx, |thread, cx| {
+            thread.send(message_2_id.clone(), ["Message 2"], cx)
+        })
+        .unwrap();
+    cx.run_until_parked();
+    fake_model.send_last_completion_stream_text_chunk("Response 2");
+    fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
+        language_model::TokenUsage {
+            input_tokens: 250,
+            output_tokens: 75,
+            cache_creation_input_tokens: 0,
+            cache_read_input_tokens: 0,
+        },
+    ));
+    fake_model.end_last_completion_stream();
+    cx.run_until_parked();
+
+    // Verify initial state
+    thread.read_with(cx, |thread, _| {
+        assert_eq!(thread.tokens_before_message(&message_2_id), Some(100));
+    });
+
+    // Truncate at message 2 (removes message 2 and everything after)
+    thread
+        .update(cx, |thread, cx| thread.truncate(message_2_id.clone(), cx))
+        .unwrap();
+    cx.run_until_parked();
+
+    // After truncation, message_2_id no longer exists, so lookup should return None
+    thread.read_with(cx, |thread, _| {
+        assert_eq!(
+            thread.tokens_before_message(&message_2_id),
+            None,
+            "After truncation, message 2 no longer exists"
+        );
+        // Message 1 still exists but has no tokens before it
+        assert_eq!(
+            thread.tokens_before_message(&message_1_id),
+            None,
+            "First message still has no tokens before it"
+        );
+    });
+}

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

@@ -1095,6 +1095,28 @@ impl Thread {
         })
     }
 
+    /// Get the total input token count as of the message before the given message.
+    ///
+    /// Returns `None` if:
+    /// - `target_id` is the first message (no previous message)
+    /// - The previous message hasn't received a response yet (no usage data)
+    /// - `target_id` is not found in the messages
+    pub fn tokens_before_message(&self, target_id: &UserMessageId) -> Option<u64> {
+        let mut previous_user_message_id: Option<&UserMessageId> = None;
+
+        for message in &self.messages {
+            if let Message::User(user_msg) = message {
+                if &user_msg.id == target_id {
+                    let prev_id = previous_user_message_id?;
+                    let usage = self.request_token_usage.get(prev_id)?;
+                    return Some(usage.input_tokens);
+                }
+                previous_user_message_id = Some(&user_msg.id);
+            }
+        }
+        None
+    }
+
     /// Look up the active profile and resolve its preferred model if one is configured.
     fn resolve_profile_model(
         profile_id: &AgentProfileId,
@@ -1703,6 +1725,10 @@ impl Thread {
         self.pending_summary_generation.is_some()
     }
 
+    pub fn is_generating_title(&self) -> bool {
+        self.pending_title_generation.is_some()
+    }
+
     pub fn summary(&mut self, cx: &mut Context<Self>) -> Shared<Task<Option<SharedString>>> {
         if let Some(summary) = self.summary.as_ref() {
             return Task::ready(Some(summary.clone())).shared();
@@ -1770,7 +1796,7 @@ impl Thread {
         task
     }
 
-    fn generate_title(&mut self, cx: &mut Context<Self>) {
+    pub fn generate_title(&mut self, cx: &mut Context<Self>) {
         let Some(model) = self.summarization_model.clone() else {
             return;
         };

crates/agent/src/tools/context_server_registry.rs πŸ”—

@@ -2,7 +2,7 @@ use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream};
 use agent_client_protocol::ToolKind;
 use anyhow::{Result, anyhow, bail};
 use collections::{BTreeMap, HashMap};
-use context_server::ContextServerId;
+use context_server::{ContextServerId, client::NotificationSubscription};
 use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
 use project::context_server_store::{ContextServerStatus, ContextServerStore};
 use std::sync::Arc;
@@ -31,17 +31,7 @@ struct RegisteredContextServer {
     prompts: BTreeMap<SharedString, ContextServerPrompt>,
     load_tools: Task<Result<()>>,
     load_prompts: Task<Result<()>>,
-}
-
-impl RegisteredContextServer {
-    fn new() -> Self {
-        Self {
-            tools: BTreeMap::default(),
-            prompts: BTreeMap::default(),
-            load_tools: Task::ready(Ok(())),
-            load_prompts: Task::ready(Ok(())),
-        }
-    }
+    _tools_updated_subscription: Option<NotificationSubscription>,
 }
 
 impl ContextServerRegistry {
@@ -111,10 +101,57 @@ impl ContextServerRegistry {
     fn get_or_register_server(
         &mut self,
         server_id: &ContextServerId,
+        cx: &mut Context<Self>,
     ) -> &mut RegisteredContextServer {
         self.registered_servers
             .entry(server_id.clone())
-            .or_insert_with(RegisteredContextServer::new)
+            .or_insert_with(|| Self::init_registered_server(server_id, &self.server_store, cx))
+    }
+
+    fn init_registered_server(
+        server_id: &ContextServerId,
+        server_store: &Entity<ContextServerStore>,
+        cx: &mut Context<Self>,
+    ) -> RegisteredContextServer {
+        let tools_updated_subscription = server_store
+            .read(cx)
+            .get_running_server(server_id)
+            .and_then(|server| {
+                let client = server.client()?;
+
+                if !client.capable(context_server::protocol::ServerCapability::Tools) {
+                    return None;
+                }
+
+                let server_id = server.id();
+                let this = cx.entity().downgrade();
+
+                Some(client.on_notification(
+                    "notifications/tools/list_changed",
+                    Box::new(move |_params, cx: AsyncApp| {
+                        let server_id = server_id.clone();
+                        let this = this.clone();
+                        cx.spawn(async move |cx| {
+                            this.update(cx, |this, cx| {
+                                log::info!(
+                                    "Received tools/list_changed notification for server {}",
+                                    server_id
+                                );
+                                this.reload_tools_for_server(server_id, cx);
+                            })
+                        })
+                        .detach();
+                    }),
+                ))
+            });
+
+        RegisteredContextServer {
+            tools: BTreeMap::default(),
+            prompts: BTreeMap::default(),
+            load_tools: Task::ready(Ok(())),
+            load_prompts: Task::ready(Ok(())),
+            _tools_updated_subscription: tools_updated_subscription,
+        }
     }
 
     fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
@@ -124,11 +161,12 @@ impl ContextServerRegistry {
         let Some(client) = server.client() else {
             return;
         };
+
         if !client.capable(context_server::protocol::ServerCapability::Tools) {
             return;
         }
 
-        let registered_server = self.get_or_register_server(&server_id);
+        let registered_server = self.get_or_register_server(&server_id, cx);
         registered_server.load_tools = cx.spawn(async move |this, cx| {
             let response = client
                 .request::<context_server::types::requests::ListTools>(())
@@ -167,7 +205,7 @@ impl ContextServerRegistry {
             return;
         }
 
-        let registered_server = self.get_or_register_server(&server_id);
+        let registered_server = self.get_or_register_server(&server_id, cx);
 
         registered_server.load_prompts = cx.spawn(async move |this, cx| {
             let response = client

crates/agent_settings/Cargo.toml πŸ”—

@@ -12,6 +12,7 @@ workspace = true
 path = "src/agent_settings.rs"
 
 [dependencies]
+agent-client-protocol.workspace = true
 anyhow.workspace = true
 cloud_llm_client.workspace = true
 collections.workspace = true

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

@@ -2,7 +2,8 @@ mod agent_profile;
 
 use std::sync::Arc;
 
-use collections::IndexMap;
+use agent_client_protocol::ModelId;
+use collections::{HashSet, IndexMap};
 use gpui::{App, Pixels, px};
 use language_model::LanguageModel;
 use project::DisableAiSettings;
@@ -33,6 +34,7 @@ pub struct AgentSettings {
     pub commit_message_model: Option<LanguageModelSelection>,
     pub thread_summary_model: Option<LanguageModelSelection>,
     pub inline_alternatives: Vec<LanguageModelSelection>,
+    pub favorite_models: Vec<LanguageModelSelection>,
     pub default_profile: AgentProfileId,
     pub default_view: DefaultAgentView,
     pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
@@ -96,6 +98,13 @@ impl AgentSettings {
     pub fn set_message_editor_max_lines(&self) -> usize {
         self.message_editor_min_lines * 2
     }
+
+    pub fn favorite_model_ids(&self) -> HashSet<ModelId> {
+        self.favorite_models
+            .iter()
+            .map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model)))
+            .collect()
+    }
 }
 
 #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
@@ -164,6 +173,7 @@ impl Settings for AgentSettings {
             commit_message_model: agent.commit_message_model,
             thread_summary_model: agent.thread_summary_model,
             inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
+            favorite_models: agent.favorite_models,
             default_profile: AgentProfileId(agent.default_profile.unwrap()),
             default_view: agent.default_view.unwrap(),
             profiles: agent

crates/agent_ui/Cargo.toml πŸ”—

@@ -13,7 +13,7 @@ path = "src/agent_ui.rs"
 doctest = false
 
 [features]
-test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support"]
+test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support", "agent/test-support"]
 unit-eval = []
 
 [dependencies]

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

@@ -34,7 +34,7 @@ use theme::ThemeSettings;
 use ui::prelude::*;
 use util::{ResultExt, debug_panic};
 use workspace::{CollaboratorId, Workspace};
-use zed_actions::agent::Chat;
+use zed_actions::agent::{Chat, PasteRaw};
 
 pub struct MessageEditor {
     mention_set: Entity<MentionSet>,
@@ -543,6 +543,9 @@ impl MessageEditor {
     }
 
     fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return;
+        };
         let editor_clipboard_selections = cx
             .read_from_clipboard()
             .and_then(|item| item.entries().first().cloned())
@@ -553,133 +556,127 @@ impl MessageEditor {
                 _ => None,
             });
 
-        let has_file_context = editor_clipboard_selections
-            .as_ref()
-            .is_some_and(|selections| {
-                selections
-                    .iter()
-                    .any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
-            });
+        // Insert creases for pasted clipboard selections that:
+        // 1. Contain exactly one selection
+        // 2. Have an associated file path
+        // 3. Span multiple lines (not single-line selections)
+        // 4. Belong to a file that exists in the current project
+        let should_insert_creases = util::maybe!({
+            let selections = editor_clipboard_selections.as_ref()?;
+            if selections.len() > 1 {
+                return Some(false);
+            }
+            let selection = selections.first()?;
+            let file_path = selection.file_path.as_ref()?;
+            let line_range = selection.line_range.as_ref()?;
 
-        if has_file_context {
-            if let Some((workspace, selections)) =
-                self.workspace.upgrade().zip(editor_clipboard_selections)
-            {
-                let Some(first_selection) = selections.first() else {
-                    return;
-                };
-                if let Some(file_path) = &first_selection.file_path {
-                    // In case someone pastes selections from another window
-                    // with a different project, we don't want to insert the
-                    // crease (containing the absolute path) since the agent
-                    // cannot access files outside the project.
-                    let is_in_project = workspace
-                        .read(cx)
-                        .project()
-                        .read(cx)
-                        .project_path_for_absolute_path(file_path, cx)
-                        .is_some();
-                    if !is_in_project {
-                        return;
-                    }
-                }
+            if line_range.start() == line_range.end() {
+                return Some(false);
+            }
 
-                cx.stop_propagation();
-                let insertion_target = self
-                    .editor
+            Some(
+                workspace
                     .read(cx)
-                    .selections
-                    .newest_anchor()
-                    .start
-                    .text_anchor;
-
-                let project = workspace.read(cx).project().clone();
-                for selection in selections {
-                    if let (Some(file_path), Some(line_range)) =
-                        (selection.file_path, selection.line_range)
-                    {
-                        let crease_text =
-                            acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
+                    .project()
+                    .read(cx)
+                    .project_path_for_absolute_path(file_path, cx)
+                    .is_some(),
+            )
+        })
+        .unwrap_or(false);
 
-                        let mention_uri = MentionUri::Selection {
-                            abs_path: Some(file_path.clone()),
-                            line_range: line_range.clone(),
-                        };
+        if should_insert_creases && let Some(selections) = editor_clipboard_selections {
+            cx.stop_propagation();
+            let insertion_target = self
+                .editor
+                .read(cx)
+                .selections
+                .newest_anchor()
+                .start
+                .text_anchor;
+
+            let project = workspace.read(cx).project().clone();
+            for selection in selections {
+                if let (Some(file_path), Some(line_range)) =
+                    (selection.file_path, selection.line_range)
+                {
+                    let crease_text =
+                        acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
 
-                        let mention_text = mention_uri.as_link().to_string();
-                        let (excerpt_id, text_anchor, content_len) =
-                            self.editor.update(cx, |editor, cx| {
-                                let buffer = editor.buffer().read(cx);
-                                let snapshot = buffer.snapshot(cx);
-                                let (excerpt_id, _, buffer_snapshot) =
-                                    snapshot.as_singleton().unwrap();
-                                let text_anchor = insertion_target.bias_left(&buffer_snapshot);
-
-                                editor.insert(&mention_text, window, cx);
-                                editor.insert(" ", window, cx);
-
-                                (*excerpt_id, text_anchor, mention_text.len())
-                            });
-
-                        let Some((crease_id, tx)) = insert_crease_for_mention(
-                            excerpt_id,
-                            text_anchor,
-                            content_len,
-                            crease_text.into(),
-                            mention_uri.icon_path(cx),
-                            None,
-                            self.editor.clone(),
-                            window,
-                            cx,
-                        ) else {
-                            continue;
-                        };
-                        drop(tx);
-
-                        let mention_task = cx
-                            .spawn({
-                                let project = project.clone();
-                                async move |_, cx| {
-                                    let project_path = project
-                                        .update(cx, |project, cx| {
-                                            project.project_path_for_absolute_path(&file_path, cx)
-                                        })
-                                        .map_err(|e| e.to_string())?
-                                        .ok_or_else(|| "project path not found".to_string())?;
-
-                                    let buffer = project
-                                        .update(cx, |project, cx| {
-                                            project.open_buffer(project_path, cx)
-                                        })
-                                        .map_err(|e| e.to_string())?
-                                        .await
-                                        .map_err(|e| e.to_string())?;
-
-                                    buffer
-                                        .update(cx, |buffer, cx| {
-                                            let start = Point::new(*line_range.start(), 0)
-                                                .min(buffer.max_point());
-                                            let end = Point::new(*line_range.end() + 1, 0)
-                                                .min(buffer.max_point());
-                                            let content =
-                                                buffer.text_for_range(start..end).collect();
-                                            Mention::Text {
-                                                content,
-                                                tracked_buffers: vec![cx.entity()],
-                                            }
-                                        })
-                                        .map_err(|e| e.to_string())
-                                }
-                            })
-                            .shared();
+                    let mention_uri = MentionUri::Selection {
+                        abs_path: Some(file_path.clone()),
+                        line_range: line_range.clone(),
+                    };
+
+                    let mention_text = mention_uri.as_link().to_string();
+                    let (excerpt_id, text_anchor, content_len) =
+                        self.editor.update(cx, |editor, cx| {
+                            let buffer = editor.buffer().read(cx);
+                            let snapshot = buffer.snapshot(cx);
+                            let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
+                            let text_anchor = insertion_target.bias_left(&buffer_snapshot);
+
+                            editor.insert(&mention_text, window, cx);
+                            editor.insert(" ", window, cx);
 
-                        self.mention_set.update(cx, |mention_set, _cx| {
-                            mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
+                            (*excerpt_id, text_anchor, mention_text.len())
                         });
-                    }
+
+                    let Some((crease_id, tx)) = insert_crease_for_mention(
+                        excerpt_id,
+                        text_anchor,
+                        content_len,
+                        crease_text.into(),
+                        mention_uri.icon_path(cx),
+                        None,
+                        self.editor.clone(),
+                        window,
+                        cx,
+                    ) else {
+                        continue;
+                    };
+                    drop(tx);
+
+                    let mention_task = cx
+                        .spawn({
+                            let project = project.clone();
+                            async move |_, cx| {
+                                let project_path = project
+                                    .update(cx, |project, cx| {
+                                        project.project_path_for_absolute_path(&file_path, cx)
+                                    })
+                                    .map_err(|e| e.to_string())?
+                                    .ok_or_else(|| "project path not found".to_string())?;
+
+                                let buffer = project
+                                    .update(cx, |project, cx| project.open_buffer(project_path, cx))
+                                    .map_err(|e| e.to_string())?
+                                    .await
+                                    .map_err(|e| e.to_string())?;
+
+                                buffer
+                                    .update(cx, |buffer, cx| {
+                                        let start = Point::new(*line_range.start(), 0)
+                                            .min(buffer.max_point());
+                                        let end = Point::new(*line_range.end() + 1, 0)
+                                            .min(buffer.max_point());
+                                        let content = buffer.text_for_range(start..end).collect();
+                                        Mention::Text {
+                                            content,
+                                            tracked_buffers: vec![cx.entity()],
+                                        }
+                                    })
+                                    .map_err(|e| e.to_string())
+                            }
+                        })
+                        .shared();
+
+                    self.mention_set.update(cx, |mention_set, _cx| {
+                        mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
+                    });
                 }
-                return;
             }
+            return;
         }
 
         if self.prompt_capabilities.borrow().image
@@ -690,6 +687,13 @@ impl MessageEditor {
         }
     }
 
+    fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
+        let editor = self.editor.clone();
+        window.defer(cx, move |window, cx| {
+            editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
+        });
+    }
+
     pub fn insert_dragged_files(
         &mut self,
         paths: Vec<project::ProjectPath>,
@@ -967,6 +971,7 @@ impl Render for MessageEditor {
             .on_action(cx.listener(Self::chat))
             .on_action(cx.listener(Self::chat_with_follow))
             .on_action(cx.listener(Self::cancel))
+            .on_action(cx.listener(Self::paste_raw))
             .capture_action(cx.listener(Self::paste))
             .flex_1()
             .child({
@@ -1365,7 +1370,7 @@ mod tests {
                     cx,
                 );
             });
-            message_editor.read(cx).focus_handle(cx).focus(window);
+            message_editor.read(cx).focus_handle(cx).focus(window, cx);
             message_editor.read(cx).editor().clone()
         });
 
@@ -1587,7 +1592,7 @@ mod tests {
                     cx,
                 );
             });
-            message_editor.read(cx).focus_handle(cx).focus(window);
+            message_editor.read(cx).focus_handle(cx).focus(window, cx);
             let editor = message_editor.read(cx).editor().clone();
             (message_editor, editor)
         });
@@ -2315,7 +2320,7 @@ mod tests {
                     cx,
                 );
             });
-            message_editor.read(cx).focus_handle(cx).focus(window);
+            message_editor.read(cx).focus_handle(cx).focus(window, cx);
             let editor = message_editor.read(cx).editor().clone();
             (message_editor, editor)
         });

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

@@ -1,18 +1,22 @@
 use std::{cmp::Reverse, rc::Rc, sync::Arc};
 
 use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
+use agent_client_protocol::ModelId;
 use agent_servers::AgentServer;
+use agent_settings::AgentSettings;
 use anyhow::Result;
-use collections::IndexMap;
+use collections::{HashSet, IndexMap};
 use fs::Fs;
 use futures::FutureExt;
 use fuzzy::{StringMatchCandidate, match_strings};
 use gpui::{
     Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
 };
+use itertools::Itertools;
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate};
-use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, prelude::*};
+use settings::Settings;
+use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, prelude::*};
 use util::ResultExt;
 use zed_actions::agent::OpenSettings;
 
@@ -38,7 +42,7 @@ pub fn acp_model_selector(
 
 enum AcpModelPickerEntry {
     Separator(SharedString),
-    Model(AgentModelInfo),
+    Model(AgentModelInfo, bool),
 }
 
 pub struct AcpModelPickerDelegate {
@@ -115,6 +119,67 @@ impl AcpModelPickerDelegate {
     pub fn active_model(&self) -> Option<&AgentModelInfo> {
         self.selected_model.as_ref()
     }
+
+    pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        if !self.selector.supports_favorites() {
+            return;
+        }
+
+        let favorites = AgentSettings::get_global(cx).favorite_model_ids();
+
+        if favorites.is_empty() {
+            return;
+        }
+
+        let Some(models) = self.models.clone() else {
+            return;
+        };
+
+        let all_models: Vec<AgentModelInfo> = match models {
+            AgentModelList::Flat(list) => list,
+            AgentModelList::Grouped(index_map) => index_map
+                .into_values()
+                .flatten()
+                .collect::<Vec<AgentModelInfo>>(),
+        };
+
+        let favorite_models = all_models
+            .iter()
+            .filter(|model| favorites.contains(&model.id))
+            .unique_by(|model| &model.id)
+            .cloned()
+            .collect::<Vec<_>>();
+
+        let current_id = self.selected_model.as_ref().map(|m| m.id.clone());
+
+        let current_index_in_favorites = current_id
+            .as_ref()
+            .and_then(|id| favorite_models.iter().position(|m| &m.id == id))
+            .unwrap_or(usize::MAX);
+
+        let next_index = if current_index_in_favorites == usize::MAX {
+            0
+        } else {
+            (current_index_in_favorites + 1) % favorite_models.len()
+        };
+
+        let next_model = favorite_models[next_index].clone();
+
+        self.selector
+            .select_model(next_model.id.clone(), cx)
+            .detach_and_log_err(cx);
+
+        self.selected_model = Some(next_model);
+
+        // Keep the picker selection aligned with the newly-selected model
+        if let Some(new_index) = self.filtered_entries.iter().position(|entry| {
+            matches!(entry, AcpModelPickerEntry::Model(model_info, _) if self.selected_model.as_ref().is_some_and(|selected| model_info.id == selected.id))
+        }) {
+            self.set_selected_index(new_index, window, cx);
+        } else {
+            cx.notify();
+        }
+    }
 }
 
 impl PickerDelegate for AcpModelPickerDelegate {
@@ -140,7 +205,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
         _cx: &mut Context<Picker<Self>>,
     ) -> bool {
         match self.filtered_entries.get(ix) {
-            Some(AcpModelPickerEntry::Model(_)) => true,
+            Some(AcpModelPickerEntry::Model(_, _)) => true,
             Some(AcpModelPickerEntry::Separator(_)) | None => false,
         }
     }
@@ -155,6 +220,12 @@ impl PickerDelegate for AcpModelPickerDelegate {
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Task<()> {
+        let favorites = if self.selector.supports_favorites() {
+            AgentSettings::get_global(cx).favorite_model_ids()
+        } else {
+            Default::default()
+        };
+
         cx.spawn_in(window, async move |this, cx| {
             let filtered_models = match this
                 .read_with(cx, |this, cx| {
@@ -171,7 +242,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
 
             this.update_in(cx, |this, window, cx| {
                 this.delegate.filtered_entries =
-                    info_list_to_picker_entries(filtered_models).collect();
+                    info_list_to_picker_entries(filtered_models, &favorites);
                 // Finds the currently selected model in the list
                 let new_index = this
                     .delegate
@@ -179,7 +250,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
                     .as_ref()
                     .and_then(|selected| {
                         this.delegate.filtered_entries.iter().position(|entry| {
-                            if let AcpModelPickerEntry::Model(model_info) = entry {
+                            if let AcpModelPickerEntry::Model(model_info, _) = entry {
                                 model_info.id == selected.id
                             } else {
                                 false
@@ -195,7 +266,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
     }
 
     fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        if let Some(AcpModelPickerEntry::Model(model_info)) =
+        if let Some(AcpModelPickerEntry::Model(model_info, _)) =
             self.filtered_entries.get(self.selected_index)
         {
             if window.modifiers().secondary() {
@@ -233,7 +304,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
     fn render_match(
         &self,
         ix: usize,
-        is_focused: bool,
+        selected: bool,
         _: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
@@ -241,32 +312,53 @@ impl PickerDelegate for AcpModelPickerDelegate {
             AcpModelPickerEntry::Separator(title) => {
                 Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())
             }
-            AcpModelPickerEntry::Model(model_info) => {
+            AcpModelPickerEntry::Model(model_info, is_favorite) => {
                 let is_selected = Some(model_info) == self.selected_model.as_ref();
                 let default_model = self.agent_server.default_model(cx);
                 let is_default = default_model.as_ref() == Some(&model_info.id);
 
+                let supports_favorites = self.selector.supports_favorites();
+
+                let is_favorite = *is_favorite;
+                let handle_action_click = {
+                    let model_id = model_info.id.clone();
+                    let fs = self.fs.clone();
+
+                    move |cx: &App| {
+                        crate::favorite_models::toggle_model_id_in_settings(
+                            model_id.clone(),
+                            !is_favorite,
+                            fs.clone(),
+                            cx,
+                        );
+                    }
+                };
+
                 Some(
                     div()
                         .id(("model-picker-menu-child", ix))
                         .when_some(model_info.description.clone(), |this, description| {
-                            this
-                                .on_hover(cx.listener(move |menu, hovered, _, cx| {
-                                    if *hovered {
-                                        menu.delegate.selected_description = Some((ix, description.clone(), is_default));
-                                    } else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) {
-                                        menu.delegate.selected_description = None;
-                                    }
-                                    cx.notify();
-                                }))
+                            this.on_hover(cx.listener(move |menu, hovered, _, cx| {
+                                if *hovered {
+                                    menu.delegate.selected_description =
+                                        Some((ix, description.clone(), is_default));
+                                } else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) {
+                                    menu.delegate.selected_description = None;
+                                }
+                                cx.notify();
+                            }))
                         })
                         .child(
                             ModelSelectorListItem::new(ix, model_info.name.clone())
-                                .is_focused(is_focused)
+                                .when_some(model_info.icon, |this, icon| this.icon(icon))
                                 .is_selected(is_selected)
-                                .when_some(model_info.icon, |this, icon| this.icon(icon)),
+                                .is_focused(selected)
+                                .when(supports_favorites, |this| {
+                                    this.is_favorite(is_favorite)
+                                        .on_toggle_favorite(handle_action_click)
+                                }),
                         )
-                        .into_any_element()
+                        .into_any_element(),
                 )
             }
         }
@@ -314,18 +406,51 @@ impl PickerDelegate for AcpModelPickerDelegate {
 
 fn info_list_to_picker_entries(
     model_list: AgentModelList,
-) -> impl Iterator<Item = AcpModelPickerEntry> {
+    favorites: &HashSet<ModelId>,
+) -> Vec<AcpModelPickerEntry> {
+    let mut entries = Vec::new();
+
+    let all_models: Vec<_> = match &model_list {
+        AgentModelList::Flat(list) => list.iter().collect(),
+        AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
+    };
+
+    let favorite_models: Vec<_> = all_models
+        .iter()
+        .filter(|m| favorites.contains(&m.id))
+        .unique_by(|m| &m.id)
+        .collect();
+
+    let has_favorites = !favorite_models.is_empty();
+    if has_favorites {
+        entries.push(AcpModelPickerEntry::Separator("Favorite".into()));
+        for model in favorite_models {
+            entries.push(AcpModelPickerEntry::Model((*model).clone(), true));
+        }
+    }
+
     match model_list {
         AgentModelList::Flat(list) => {
-            itertools::Either::Left(list.into_iter().map(AcpModelPickerEntry::Model))
+            if has_favorites {
+                entries.push(AcpModelPickerEntry::Separator("All".into()));
+            }
+            for model in list {
+                let is_favorite = favorites.contains(&model.id);
+                entries.push(AcpModelPickerEntry::Model(model, is_favorite));
+            }
         }
         AgentModelList::Grouped(index_map) => {
-            itertools::Either::Right(index_map.into_iter().flat_map(|(group_name, models)| {
-                std::iter::once(AcpModelPickerEntry::Separator(group_name.0))
-                    .chain(models.into_iter().map(AcpModelPickerEntry::Model))
-            }))
+            for (group_name, models) in index_map {
+                entries.push(AcpModelPickerEntry::Separator(group_name.0));
+                for model in models {
+                    let is_favorite = favorites.contains(&model.id);
+                    entries.push(AcpModelPickerEntry::Model(model, is_favorite));
+                }
+            }
         }
     }
+
+    entries
 }
 
 async fn fuzzy_search(
@@ -447,6 +572,168 @@ mod tests {
         }
     }
 
+    fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
+        models
+            .into_iter()
+            .map(|m| ModelId::new(m.to_string()))
+            .collect()
+    }
+
+    fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
+        entries
+            .iter()
+            .filter_map(|entry| match entry {
+                AcpModelPickerEntry::Model(info, _) => Some(info.id.0.as_ref()),
+                _ => None,
+            })
+            .collect()
+    }
+
+    fn get_entry_labels(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
+        entries
+            .iter()
+            .map(|entry| match entry {
+                AcpModelPickerEntry::Model(info, _) => info.id.0.as_ref(),
+                AcpModelPickerEntry::Separator(s) => &s,
+            })
+            .collect()
+    }
+
+    #[gpui::test]
+    fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
+        let models = create_model_list(vec![
+            ("zed", vec!["zed/claude", "zed/gemini"]),
+            ("openai", vec!["openai/gpt-5"]),
+        ]);
+        let favorites = create_favorites(vec!["zed/gemini"]);
+
+        let entries = info_list_to_picker_entries(models, &favorites);
+
+        assert!(matches!(
+            entries.first(),
+            Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
+        ));
+
+        let model_ids = get_entry_model_ids(&entries);
+        assert_eq!(model_ids[0], "zed/gemini");
+    }
+
+    #[gpui::test]
+    fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
+        let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
+        let favorites = create_favorites(vec![]);
+
+        let entries = info_list_to_picker_entries(models, &favorites);
+
+        assert!(matches!(
+            entries.first(),
+            Some(AcpModelPickerEntry::Separator(s)) if s == "zed"
+        ));
+    }
+
+    #[gpui::test]
+    fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
+        let models = create_model_list(vec![
+            ("zed", vec!["zed/claude", "zed/gemini"]),
+            ("openai", vec!["openai/gpt-5"]),
+        ]);
+        let favorites = create_favorites(vec!["zed/claude"]);
+
+        let entries = info_list_to_picker_entries(models, &favorites);
+
+        for entry in &entries {
+            if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
+                if info.id.0.as_ref() == "zed/claude" {
+                    assert!(is_favorite, "zed/claude should be a favorite");
+                } else {
+                    assert!(!is_favorite, "{} should not be a favorite", info.id.0);
+                }
+            }
+        }
+    }
+
+    #[gpui::test]
+    fn test_favorites_appear_in_both_sections(_cx: &mut TestAppContext) {
+        let models = create_model_list(vec![
+            ("zed", vec!["zed/claude", "zed/gemini"]),
+            ("openai", vec!["openai/gpt-5", "openai/gpt-4"]),
+        ]);
+        let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
+
+        let entries = info_list_to_picker_entries(models, &favorites);
+        let model_ids = get_entry_model_ids(&entries);
+
+        assert_eq!(model_ids[0], "zed/gemini");
+        assert_eq!(model_ids[1], "openai/gpt-5");
+
+        assert!(model_ids[2..].contains(&"zed/gemini"));
+        assert!(model_ids[2..].contains(&"openai/gpt-5"));
+    }
+
+    #[gpui::test]
+    fn test_favorites_are_not_duplicated_when_repeated_in_other_sections(_cx: &mut TestAppContext) {
+        let models = create_model_list(vec![
+            ("Recommended", vec!["zed/claude", "anthropic/claude"]),
+            ("Zed", vec!["zed/claude", "zed/gpt-5"]),
+            ("Antropic", vec!["anthropic/claude"]),
+            ("OpenAI", vec!["openai/gpt-5"]),
+        ]);
+
+        let favorites = create_favorites(vec!["zed/claude"]);
+
+        let entries = info_list_to_picker_entries(models, &favorites);
+        let labels = get_entry_labels(&entries);
+
+        assert_eq!(
+            labels,
+            vec![
+                "Favorite",
+                "zed/claude",
+                "Recommended",
+                "zed/claude",
+                "anthropic/claude",
+                "Zed",
+                "zed/claude",
+                "zed/gpt-5",
+                "Antropic",
+                "anthropic/claude",
+                "OpenAI",
+                "openai/gpt-5"
+            ]
+        );
+    }
+
+    #[gpui::test]
+    fn test_flat_model_list_with_favorites(_cx: &mut TestAppContext) {
+        let models = AgentModelList::Flat(vec![
+            acp_thread::AgentModelInfo {
+                id: acp::ModelId::new("zed/claude".to_string()),
+                name: "Claude".into(),
+                description: None,
+                icon: None,
+            },
+            acp_thread::AgentModelInfo {
+                id: acp::ModelId::new("zed/gemini".to_string()),
+                name: "Gemini".into(),
+                description: None,
+                icon: None,
+            },
+        ]);
+        let favorites = create_favorites(vec!["zed/gemini"]);
+
+        let entries = info_list_to_picker_entries(models, &favorites);
+
+        assert!(matches!(
+            entries.first(),
+            Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
+        ));
+
+        assert!(entries.iter().any(|e| matches!(
+            e,
+            AcpModelPickerEntry::Separator(s) if s == "All"
+        )));
+    }
+
     #[gpui::test]
     async fn test_fuzzy_match(cx: &mut TestAppContext) {
         let models = create_model_list(vec![

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

@@ -3,15 +3,15 @@ use std::sync::Arc;
 
 use acp_thread::{AgentModelInfo, AgentModelSelector};
 use agent_servers::AgentServer;
+use agent_settings::AgentSettings;
 use fs::Fs;
 use gpui::{Entity, FocusHandle};
 use picker::popover_menu::PickerPopoverMenu;
-use ui::{
-    ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, TintColor, Tooltip, Window,
-    prelude::*,
-};
+use settings::Settings as _;
+use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
 use zed_actions::agent::ToggleModelSelector;
 
+use crate::CycleFavoriteModels;
 use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
 
 pub struct AcpModelSelectorPopover {
@@ -54,6 +54,12 @@ impl AcpModelSelectorPopover {
     pub fn active_model<'a>(&self, cx: &'a App) -> Option<&'a AgentModelInfo> {
         self.selector.read(cx).delegate.active_model()
     }
+
+    pub fn cycle_favorite_models(&self, window: &mut Window, cx: &mut Context<Self>) {
+        self.selector.update(cx, |selector, cx| {
+            selector.delegate.cycle_favorite_models(window, cx);
+        });
+    }
 }
 
 impl Render for AcpModelSelectorPopover {
@@ -74,6 +80,46 @@ impl Render for AcpModelSelectorPopover {
             (Color::Muted, IconName::ChevronDown)
         };
 
+        let tooltip = Tooltip::element({
+            move |_, cx| {
+                let focus_handle = focus_handle.clone();
+                let should_show_cycle_row = !AgentSettings::get_global(cx)
+                    .favorite_model_ids()
+                    .is_empty();
+
+                v_flex()
+                    .gap_1()
+                    .child(
+                        h_flex()
+                            .gap_2()
+                            .justify_between()
+                            .child(Label::new("Change Model"))
+                            .child(KeyBinding::for_action_in(
+                                &ToggleModelSelector,
+                                &focus_handle,
+                                cx,
+                            )),
+                    )
+                    .when(should_show_cycle_row, |this| {
+                        this.child(
+                            h_flex()
+                                .pt_1()
+                                .gap_2()
+                                .border_t_1()
+                                .border_color(cx.theme().colors().border_variant)
+                                .justify_between()
+                                .child(Label::new("Cycle Favorited Models"))
+                                .child(KeyBinding::for_action_in(
+                                    &CycleFavoriteModels,
+                                    &focus_handle,
+                                    cx,
+                                )),
+                        )
+                    })
+                    .into_any()
+            }
+        });
+
         PickerPopoverMenu::new(
             self.selector.clone(),
             ButtonLike::new("active-model")
@@ -88,9 +134,7 @@ impl Render for AcpModelSelectorPopover {
                         .ml_0p5(),
                 )
                 .child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)),
-            move |_window, cx| {
-                Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
-            },
+            tooltip,
             gpui::Corner::BottomRight,
             cx,
         )

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

@@ -34,7 +34,7 @@ use language::Buffer;
 
 use language_model::LanguageModelRegistry;
 use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
-use project::{Project, ProjectEntryId};
+use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
 use prompt_store::{PromptId, PromptStore};
 use rope::Point;
 use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
@@ -66,8 +66,8 @@ use crate::profile_selector::{ProfileProvider, ProfileSelector};
 use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip, UsageCallout};
 use crate::{
     AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
-    CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory,
-    RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
+    CycleFavoriteModels, CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread,
+    OpenAgentDiff, OpenHistory, RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
 };
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -253,13 +253,14 @@ impl ThreadFeedbackState {
             editor
         });
 
-        editor.read(cx).focus_handle(cx).focus(window);
+        editor.read(cx).focus_handle(cx).focus(window, cx);
         editor
     }
 }
 
 pub struct AcpThreadView {
     agent: Rc<dyn AgentServer>,
+    agent_server_store: Entity<AgentServerStore>,
     workspace: WeakEntity<Workspace>,
     project: Entity<Project>,
     thread_state: ThreadState,
@@ -337,7 +338,13 @@ impl AcpThreadView {
         let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
         let available_commands = Rc::new(RefCell::new(vec![]));
 
-        let placeholder = placeholder_text(agent.name().as_ref(), false);
+        let agent_server_store = project.read(cx).agent_server_store().clone();
+        let agent_display_name = agent_server_store
+            .read(cx)
+            .agent_display_name(&ExternalAgentServerName(agent.name()))
+            .unwrap_or_else(|| agent.name());
+
+        let placeholder = placeholder_text(agent_display_name.as_ref(), false);
 
         let message_editor = cx.new(|cx| {
             let mut editor = MessageEditor::new(
@@ -376,7 +383,6 @@ impl AcpThreadView {
             )
         });
 
-        let agent_server_store = project.read(cx).agent_server_store().clone();
         let subscriptions = [
             cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
             cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
@@ -406,6 +412,7 @@ impl AcpThreadView {
 
         Self {
             agent: agent.clone(),
+            agent_server_store,
             workspace: workspace.clone(),
             project: project.clone(),
             entry_view_state,
@@ -682,7 +689,7 @@ impl AcpThreadView {
                             })
                         });
 
-                        this.message_editor.focus_handle(cx).focus(window);
+                        this.message_editor.focus_handle(cx).focus(window, cx);
 
                         cx.notify();
                     }
@@ -737,7 +744,7 @@ impl AcpThreadView {
         cx: &mut App,
     ) {
         let agent_name = agent.name();
-        let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
+        let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
             let registry = LanguageModelRegistry::global(cx);
 
             let sub = window.subscribe(&registry, cx, {
@@ -779,12 +786,11 @@ impl AcpThreadView {
                 configuration_view,
                 description: err
                     .description
-                    .clone()
                     .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
                 _subscription: subscription,
             };
             if this.message_editor.focus_handle(cx).is_focused(window) {
-                this.focus_handle.focus(window)
+                this.focus_handle.focus(window, cx)
             }
             cx.notify();
         })
@@ -804,7 +810,7 @@ impl AcpThreadView {
                 ThreadState::LoadError(LoadError::Other(format!("{:#}", err).into()))
         }
         if self.message_editor.focus_handle(cx).is_focused(window) {
-            self.focus_handle.focus(window)
+            self.focus_handle.focus(window, cx)
         }
         cx.notify();
     }
@@ -1088,10 +1094,7 @@ impl AcpThreadView {
                 window.defer(cx, |window, cx| {
                     Self::handle_auth_required(
                         this,
-                        AuthRequired {
-                            description: None,
-                            provider_id: None,
-                        },
+                        AuthRequired::new(),
                         agent,
                         connection,
                         window,
@@ -1270,7 +1273,7 @@ impl AcpThreadView {
                 }
             })
         };
-        self.focus_handle(cx).focus(window);
+        self.focus_handle(cx).focus(window, cx);
         cx.notify();
     }
 
@@ -1322,7 +1325,7 @@ impl AcpThreadView {
                 .await?;
             this.update_in(cx, |this, window, cx| {
                 this.send_impl(message_editor, window, cx);
-                this.focus_handle(cx).focus(window);
+                this.focus_handle(cx).focus(window, cx);
             })?;
             anyhow::Ok(())
         })
@@ -1465,7 +1468,7 @@ impl AcpThreadView {
                 self.thread_retry_status.take();
                 self.thread_state = ThreadState::LoadError(error.clone());
                 if self.message_editor.focus_handle(cx).is_focused(window) {
-                    self.focus_handle.focus(window)
+                    self.focus_handle.focus(window, cx)
                 }
             }
             AcpThreadEvent::TitleUpdated => {
@@ -1500,7 +1503,13 @@ impl AcpThreadView {
                 let has_commands = !available_commands.is_empty();
                 self.available_commands.replace(available_commands);
 
-                let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands);
+                let agent_display_name = self
+                    .agent_server_store
+                    .read(cx)
+                    .agent_display_name(&ExternalAgentServerName(self.agent.name()))
+                    .unwrap_or_else(|| self.agent.name());
+
+                let new_placeholder = placeholder_text(agent_display_name.as_ref(), has_commands);
 
                 self.message_editor.update(cx, |editor, cx| {
                     editor.set_placeholder_text(&new_placeholder, window, cx);
@@ -1663,44 +1672,6 @@ impl AcpThreadView {
                 });
                 return;
             }
-        } else if method.0.as_ref() == "anthropic-api-key" {
-            let registry = LanguageModelRegistry::global(cx);
-            let provider = registry
-                .read(cx)
-                .provider(&language_model::ANTHROPIC_PROVIDER_ID)
-                .unwrap();
-            let this = cx.weak_entity();
-            let agent = self.agent.clone();
-            let connection = connection.clone();
-            window.defer(cx, move |window, cx| {
-                if !provider.is_authenticated(cx) {
-                    Self::handle_auth_required(
-                        this,
-                        AuthRequired {
-                            description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
-                            provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
-                        },
-                        agent,
-                        connection,
-                        window,
-                        cx,
-                    );
-                } else {
-                    this.update(cx, |this, cx| {
-                        this.thread_state = Self::initial_state(
-                            agent,
-                            None,
-                            this.workspace.clone(),
-                            this.project.clone(),
-                            true,
-                            window,
-                            cx,
-                        )
-                    })
-                    .ok();
-                }
-            });
-            return;
         } else if method.0.as_ref() == "vertex-ai"
             && std::env::var("GOOGLE_API_KEY").is_err()
             && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
@@ -1898,6 +1869,17 @@ impl AcpThreadView {
         })
     }
 
+    pub fn has_user_submitted_prompt(&self, cx: &App) -> bool {
+        self.thread().is_some_and(|thread| {
+            thread.read(cx).entries().iter().any(|entry| {
+                matches!(
+                    entry,
+                    AgentThreadEntry::UserMessage(user_message) if user_message.id.is_some()
+                )
+            })
+        })
+    }
+
     fn authorize_tool_call(
         &mut self,
         tool_call_id: acp::ToolCallId,
@@ -2142,6 +2124,7 @@ impl AcpThreadView {
                 chunks,
                 indented: _,
             }) => {
+                let mut is_blank = true;
                 let is_last = entry_ix + 1 == total_entries;
 
                 let style = default_markdown_style(false, false, window, cx);
@@ -2151,36 +2134,55 @@ impl AcpThreadView {
                     .children(chunks.iter().enumerate().filter_map(
                         |(chunk_ix, chunk)| match chunk {
                             AssistantMessageChunk::Message { block } => {
-                                block.markdown().map(|md| {
-                                    self.render_markdown(md.clone(), style.clone())
-                                        .into_any_element()
+                                block.markdown().and_then(|md| {
+                                    let this_is_blank = md.read(cx).source().trim().is_empty();
+                                    is_blank = is_blank && this_is_blank;
+                                    if this_is_blank {
+                                        return None;
+                                    }
+
+                                    Some(
+                                        self.render_markdown(md.clone(), style.clone())
+                                            .into_any_element(),
+                                    )
                                 })
                             }
                             AssistantMessageChunk::Thought { block } => {
-                                block.markdown().map(|md| {
-                                    self.render_thinking_block(
-                                        entry_ix,
-                                        chunk_ix,
-                                        md.clone(),
-                                        window,
-                                        cx,
+                                block.markdown().and_then(|md| {
+                                    let this_is_blank = md.read(cx).source().trim().is_empty();
+                                    is_blank = is_blank && this_is_blank;
+                                    if this_is_blank {
+                                        return None;
+                                    }
+
+                                    Some(
+                                        self.render_thinking_block(
+                                            entry_ix,
+                                            chunk_ix,
+                                            md.clone(),
+                                            window,
+                                            cx,
+                                        )
+                                        .into_any_element(),
                                     )
-                                    .into_any_element()
                                 })
                             }
                         },
                     ))
                     .into_any();
 
-                v_flex()
-                    .px_5()
-                    .py_1p5()
-                    .when(is_first_indented, |this| this.pt_0p5())
-                    .when(is_last, |this| this.pb_4())
-                    .w_full()
-                    .text_ui(cx)
-                    .child(message_body)
-                    .into_any()
+                if is_blank {
+                    Empty.into_any()
+                } else {
+                    v_flex()
+                        .px_5()
+                        .py_1p5()
+                        .when(is_last, |this| this.pb_4())
+                        .w_full()
+                        .text_ui(cx)
+                        .child(message_body)
+                        .into_any()
+                }
             }
             AgentThreadEntry::ToolCall(tool_call) => {
                 let has_terminals = tool_call.terminals().next().is_some();
@@ -2212,7 +2214,7 @@ impl AcpThreadView {
             div()
                 .relative()
                 .w_full()
-                .pl(rems_from_px(20.0))
+                .pl_5()
                 .bg(cx.theme().colors().panel_background.opacity(0.2))
                 .child(
                     div()
@@ -2429,6 +2431,12 @@ impl AcpThreadView {
         let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
 
         let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
+        let input_output_header = |label: SharedString| {
+            Label::new(label)
+                .size(LabelSize::XSmall)
+                .color(Color::Muted)
+                .buffer_font(cx)
+        };
 
         let tool_output_display =
             if is_open {
@@ -2470,7 +2478,25 @@ impl AcpThreadView {
                     | ToolCallStatus::Completed
                     | ToolCallStatus::Failed
                     | ToolCallStatus::Canceled => v_flex()
-                        .w_full()
+                        .when(!is_edit && !is_terminal_tool, |this| {
+                            this.mt_1p5().w_full().child(
+                                v_flex()
+                                    .ml(rems(0.4))
+                                    .px_3p5()
+                                    .pb_1()
+                                    .gap_1()
+                                    .border_l_1()
+                                    .border_color(self.tool_card_border_color(cx))
+                                    .child(input_output_header("Raw Input:".into()))
+                                    .children(tool_call.raw_input_markdown.clone().map(|input| {
+                                        self.render_markdown(
+                                            input,
+                                            default_markdown_style(false, false, window, cx),
+                                        )
+                                    }))
+                                    .child(input_output_header("Output:".into())),
+                            )
+                        })
                         .children(tool_call.content.iter().enumerate().map(
                             |(content_ix, content)| {
                                 div().child(self.render_tool_call_content(
@@ -2569,7 +2595,7 @@ impl AcpThreadView {
                                         .gap_px()
                                         .when(is_collapsible, |this| {
                                             this.child(
-                                            Disclosure::new(("expand", entry_ix), is_open)
+                                            Disclosure::new(("expand-output", entry_ix), is_open)
                                                 .opened_icon(IconName::ChevronUp)
                                                 .closed_icon(IconName::ChevronDown)
                                                 .visible_on_hover(&card_header_id)
@@ -2755,20 +2781,20 @@ impl AcpThreadView {
         let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
 
         v_flex()
-            .mt_1p5()
             .gap_2()
-            .when(!card_layout, |this| {
-                this.ml(rems(0.4))
-                    .px_3p5()
-                    .border_l_1()
-                    .border_color(self.tool_card_border_color(cx))
-            })
-            .when(card_layout, |this| {
-                this.px_2().pb_2().when(context_ix > 0, |this| {
-                    this.border_t_1()
-                        .pt_2()
+            .map(|this| {
+                if card_layout {
+                    this.when(context_ix > 0, |this| {
+                        this.pt_2()
+                            .border_t_1()
+                            .border_color(self.tool_card_border_color(cx))
+                    })
+                } else {
+                    this.ml(rems(0.4))
+                        .px_3p5()
+                        .border_l_1()
                         .border_color(self.tool_card_border_color(cx))
-                })
+                }
             })
             .text_xs()
             .text_color(cx.theme().colors().text_muted)
@@ -3489,138 +3515,119 @@ impl AcpThreadView {
         pending_auth_method: Option<&acp::AuthMethodId>,
         window: &mut Window,
         cx: &Context<Self>,
-    ) -> Div {
-        let show_description =
-            configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
-
+    ) -> impl IntoElement {
         let auth_methods = connection.auth_methods();
 
-        v_flex().flex_1().size_full().justify_end().child(
-            v_flex()
-                .p_2()
-                .pr_3()
-                .w_full()
-                .gap_1()
-                .border_t_1()
-                .border_color(cx.theme().colors().border)
-                .bg(cx.theme().status().warning.opacity(0.04))
-                .child(
-                    h_flex()
-                        .gap_1p5()
-                        .child(
-                            Icon::new(IconName::Warning)
-                                .color(Color::Warning)
-                                .size(IconSize::Small),
-                        )
-                        .child(Label::new("Authentication Required").size(LabelSize::Small)),
-                )
-                .children(description.map(|desc| {
-                    div().text_ui(cx).child(self.render_markdown(
-                        desc.clone(),
-                        default_markdown_style(false, false, window, cx),
-                    ))
-                }))
-                .children(
-                    configuration_view
-                        .cloned()
-                        .map(|view| div().w_full().child(view)),
-                )
-                .when(show_description, |el| {
-                    el.child(
-                        Label::new(format!(
-                            "You are not currently authenticated with {}.{}",
-                            self.agent.name(),
-                            if auth_methods.len() > 1 {
-                                " Please choose one of the following options:"
-                            } else {
-                                ""
-                            }
-                        ))
-                        .size(LabelSize::Small)
-                        .color(Color::Muted)
-                        .mb_1()
-                        .ml_5(),
-                    )
-                })
-                .when_some(pending_auth_method, |el, _| {
-                    el.child(
-                        h_flex()
-                            .py_4()
-                            .w_full()
-                            .justify_center()
-                            .gap_1()
-                            .child(
-                                Icon::new(IconName::ArrowCircle)
-                                    .size(IconSize::Small)
-                                    .color(Color::Muted)
-                                    .with_rotate_animation(2),
-                            )
-                            .child(Label::new("Authenticating…").size(LabelSize::Small)),
-                    )
-                })
-                .when(!auth_methods.is_empty(), |this| {
-                    this.child(
-                        h_flex()
-                            .justify_end()
-                            .flex_wrap()
-                            .gap_1()
-                            .when(!show_description, |this| {
-                                this.border_t_1()
-                                    .mt_1()
-                                    .pt_2()
-                                    .border_color(cx.theme().colors().border.opacity(0.8))
+        let agent_display_name = self
+            .agent_server_store
+            .read(cx)
+            .agent_display_name(&ExternalAgentServerName(self.agent.name()))
+            .unwrap_or_else(|| self.agent.name());
+
+        let show_fallback_description = auth_methods.len() > 1
+            && configuration_view.is_none()
+            && description.is_none()
+            && pending_auth_method.is_none();
+
+        let auth_buttons = || {
+            h_flex().justify_end().flex_wrap().gap_1().children(
+                connection
+                    .auth_methods()
+                    .iter()
+                    .enumerate()
+                    .rev()
+                    .map(|(ix, method)| {
+                        let (method_id, name) = if self.project.read(cx).is_via_remote_server()
+                            && method.id.0.as_ref() == "oauth-personal"
+                            && method.name == "Log in with Google"
+                        {
+                            ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
+                        } else {
+                            (method.id.0.clone(), method.name.clone())
+                        };
+
+                        let agent_telemetry_id = connection.telemetry_id();
+
+                        Button::new(method_id.clone(), name)
+                            .label_size(LabelSize::Small)
+                            .map(|this| {
+                                if ix == 0 {
+                                    this.style(ButtonStyle::Tinted(TintColor::Accent))
+                                } else {
+                                    this.style(ButtonStyle::Outlined)
+                                }
                             })
-                            .children(connection.auth_methods().iter().enumerate().rev().map(
-                                |(ix, method)| {
-                                    let (method_id, name) = if self
-                                        .project
-                                        .read(cx)
-                                        .is_via_remote_server()
-                                        && method.id.0.as_ref() == "oauth-personal"
-                                        && method.name == "Log in with Google"
-                                    {
-                                        ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
-                                    } else {
-                                        (method.id.0.clone(), method.name.clone())
-                                    };
+                            .when_some(method.description.clone(), |this, description| {
+                                this.tooltip(Tooltip::text(description))
+                            })
+                            .on_click({
+                                cx.listener(move |this, _, window, cx| {
+                                    telemetry::event!(
+                                        "Authenticate Agent Started",
+                                        agent = agent_telemetry_id,
+                                        method = method_id
+                                    );
 
-                                    let agent_telemetry_id = connection.telemetry_id();
+                                    this.authenticate(
+                                        acp::AuthMethodId::new(method_id.clone()),
+                                        window,
+                                        cx,
+                                    )
+                                })
+                            })
+                    }),
+            )
+        };
 
-                                    Button::new(method_id.clone(), name)
-                                        .label_size(LabelSize::Small)
-                                        .map(|this| {
-                                            if ix == 0 {
-                                                this.style(ButtonStyle::Tinted(TintColor::Warning))
-                                            } else {
-                                                this.style(ButtonStyle::Outlined)
-                                            }
-                                        })
-                                        .when_some(
-                                            method.description.clone(),
-                                            |this, description| {
-                                                this.tooltip(Tooltip::text(description))
-                                            },
-                                        )
-                                        .on_click({
-                                            cx.listener(move |this, _, window, cx| {
-                                                telemetry::event!(
-                                                    "Authenticate Agent Started",
-                                                    agent = agent_telemetry_id,
-                                                    method = method_id
-                                                );
-
-                                                this.authenticate(
-                                                    acp::AuthMethodId::new(method_id.clone()),
-                                                    window,
-                                                    cx,
-                                                )
-                                            })
-                                        })
-                                },
-                            )),
-                    )
-                }),
-        )
+        if pending_auth_method.is_some() {
+            return Callout::new()
+                .icon(IconName::Info)
+                .title(format!("Authenticating to {}…", agent_display_name))
+                .actions_slot(
+                    Icon::new(IconName::ArrowCircle)
+                        .size(IconSize::Small)
+                        .color(Color::Muted)
+                        .with_rotate_animation(2)
+                        .into_any_element(),
+                )
+                .into_any_element();
+        }
+
+        Callout::new()
+            .icon(IconName::Info)
+            .title(format!("Authenticate to {}", agent_display_name))
+            .when(auth_methods.len() == 1, |this| {
+                this.actions_slot(auth_buttons())
+            })
+            .description_slot(
+                v_flex()
+                    .text_ui(cx)
+                    .map(|this| {
+                        if show_fallback_description {
+                            this.child(
+                                Label::new("Choose one of the following authentication options:")
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted),
+                            )
+                        } else {
+                            this.children(
+                                configuration_view
+                                    .cloned()
+                                    .map(|view| div().w_full().child(view)),
+                            )
+                            .children(description.map(|desc| {
+                                self.render_markdown(
+                                    desc.clone(),
+                                    default_markdown_style(false, false, window, cx),
+                                )
+                            }))
+                        }
+                    })
+                    .when(auth_methods.len() > 1, |this| {
+                        this.gap_1().child(auth_buttons())
+                    }),
+            )
+            .into_any_element()
     }
 
     fn render_load_error(
@@ -4110,6 +4117,8 @@ impl AcpThreadView {
                                 .ml_1p5()
                         });
 
+                        let full_path = path.display(path_style).to_string();
+
                         let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
                             .map(Icon::from_path)
                             .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
@@ -4143,7 +4152,6 @@ impl AcpThreadView {
                                     .relative()
                                     .pr_8()
                                     .w_full()
-                                    .overflow_x_scroll()
                                     .child(
                                         h_flex()
                                             .id(("file-name-path", index))
@@ -4155,7 +4163,14 @@ impl AcpThreadView {
                                             .child(file_icon)
                                             .children(file_name)
                                             .children(file_path)
-                                            .tooltip(Tooltip::text("Go to File"))
+                                            .tooltip(move |_, cx| {
+                                                Tooltip::with_meta(
+                                                    "Go to File",
+                                                    None,
+                                                    full_path.clone(),
+                                                    cx,
+                                                )
+                                            })
                                             .on_click({
                                                 let buffer = buffer.clone();
                                                 cx.listener(move |this, _, window, cx| {
@@ -4293,6 +4308,13 @@ impl AcpThreadView {
                         .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
                 }
             }))
+            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
+                if let Some(model_selector) = this.model_selector.as_ref() {
+                    model_selector.update(cx, |model_selector, cx| {
+                        model_selector.cycle_favorite_models(window, cx);
+                    });
+                }
+            }))
             .p_2()
             .gap_2()
             .border_t_1()
@@ -5854,10 +5876,6 @@ impl AcpThreadView {
                     };
 
                     let connection = thread.read(cx).connection().clone();
-                    let err = AuthRequired {
-                        description: None,
-                        provider_id: None,
-                    };
                     this.clear_thread_error(cx);
                     if let Some(message) = this.in_flight_prompt.take() {
                         this.message_editor.update(cx, |editor, cx| {
@@ -5866,7 +5884,14 @@ impl AcpThreadView {
                     }
                     let this = cx.weak_entity();
                     window.defer(cx, |window, cx| {
-                        Self::handle_auth_required(this, err, agent, connection, window, cx);
+                        Self::handle_auth_required(
+                            this,
+                            AuthRequired::new(),
+                            agent,
+                            connection,
+                            window,
+                            cx,
+                        );
                     })
                 }
             }))
@@ -5879,14 +5904,10 @@ impl AcpThreadView {
         };
 
         let connection = thread.read(cx).connection().clone();
-        let err = AuthRequired {
-            description: None,
-            provider_id: None,
-        };
         self.clear_thread_error(cx);
         let this = cx.weak_entity();
         window.defer(cx, |window, cx| {
-            Self::handle_auth_required(this, err, agent, connection, window, cx);
+            Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx);
         })
     }
 
@@ -5989,16 +6010,19 @@ impl Render for AcpThreadView {
                     configuration_view,
                     pending_auth_method,
                     ..
-                } => self
-                    .render_auth_required_state(
+                } => v_flex()
+                    .flex_1()
+                    .size_full()
+                    .justify_end()
+                    .child(self.render_auth_required_state(
                         connection,
                         description.as_ref(),
                         configuration_view.as_ref(),
                         pending_auth_method.as_ref(),
                         window,
                         cx,
-                    )
-                    .into_any(),
+                    ))
+                    .into_any_element(),
                 ThreadState::Loading { .. } => v_flex()
                     .flex_1()
                     .child(self.render_recent_history(cx))

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

@@ -446,17 +446,17 @@ impl AddLlmProviderModal {
             })
     }
 
-    fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
-        window.focus_next();
+    fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+        window.focus_next(cx);
     }
 
     fn on_tab_prev(
         &mut self,
         _: &menu::SelectPrevious,
         window: &mut Window,
-        _: &mut Context<Self>,
+        cx: &mut Context<Self>,
     ) {
-        window.focus_prev();
+        window.focus_prev(cx);
     }
 }
 
@@ -493,7 +493,7 @@ impl Render for AddLlmProviderModal {
             .on_action(cx.listener(Self::on_tab))
             .on_action(cx.listener(Self::on_tab_prev))
             .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
-                this.focus_handle(cx).focus(window);
+                this.focus_handle(cx).focus(window, cx);
             }))
             .child(
                 Modal::new("configure-context-server", None)

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

@@ -156,7 +156,7 @@ impl ManageProfilesModal {
             cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
                 if matches!(this.mode, Mode::ChooseProfile(_)) {
                     this.mode = Mode::choose_profile(window, cx);
-                    this.focus_handle(cx).focus(window);
+                    this.focus_handle(cx).focus(window, cx);
                     cx.notify();
                 }
             });
@@ -173,7 +173,7 @@ impl ManageProfilesModal {
 
     fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         self.mode = Mode::choose_profile(window, cx);
-        self.focus_handle(cx).focus(window);
+        self.focus_handle(cx).focus(window, cx);
     }
 
     fn new_profile(
@@ -191,7 +191,7 @@ impl ManageProfilesModal {
             name_editor,
             base_profile_id,
         });
-        self.focus_handle(cx).focus(window);
+        self.focus_handle(cx).focus(window, cx);
     }
 
     pub fn view_profile(
@@ -209,7 +209,7 @@ impl ManageProfilesModal {
             delete_profile: NavigableEntry::focusable(cx),
             cancel_item: NavigableEntry::focusable(cx),
         });
-        self.focus_handle(cx).focus(window);
+        self.focus_handle(cx).focus(window, cx);
     }
 
     fn configure_default_model(
@@ -222,7 +222,6 @@ impl ManageProfilesModal {
         let profile_id_for_closure = profile_id.clone();
 
         let model_picker = cx.new(|cx| {
-            let fs = fs.clone();
             let profile_id = profile_id_for_closure.clone();
 
             language_model_selector(
@@ -250,22 +249,36 @@ impl ManageProfilesModal {
                             })
                     }
                 },
-                move |model, cx| {
-                    let provider = model.provider_id().0.to_string();
-                    let model_id = model.id().0.to_string();
-                    let profile_id = profile_id.clone();
-
-                    update_settings_file(fs.clone(), cx, move |settings, _cx| {
-                        let agent_settings = settings.agent.get_or_insert_default();
-                        if let Some(profiles) = agent_settings.profiles.as_mut() {
-                            if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
-                                profile.default_model = Some(LanguageModelSelection {
-                                    provider: LanguageModelProviderSetting(provider.clone()),
-                                    model: model_id.clone(),
-                                });
+                {
+                    let fs = fs.clone();
+                    move |model, cx| {
+                        let provider = model.provider_id().0.to_string();
+                        let model_id = model.id().0.to_string();
+                        let profile_id = profile_id.clone();
+
+                        update_settings_file(fs.clone(), cx, move |settings, _cx| {
+                            let agent_settings = settings.agent.get_or_insert_default();
+                            if let Some(profiles) = agent_settings.profiles.as_mut() {
+                                if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
+                                    profile.default_model = Some(LanguageModelSelection {
+                                        provider: LanguageModelProviderSetting(provider.clone()),
+                                        model: model_id.clone(),
+                                    });
+                                }
                             }
-                        }
-                    });
+                        });
+                    }
+                },
+                {
+                    let fs = fs.clone();
+                    move |model, should_be_favorite, cx| {
+                        crate::favorite_models::toggle_in_settings(
+                            model,
+                            should_be_favorite,
+                            fs.clone(),
+                            cx,
+                        );
+                    }
                 },
                 false, // Do not use popover styles for the model picker
                 self.focus_handle.clone(),
@@ -287,7 +300,7 @@ impl ManageProfilesModal {
             model_picker,
             _subscription: dismiss_subscription,
         };
-        self.focus_handle(cx).focus(window);
+        self.focus_handle(cx).focus(window, cx);
     }
 
     fn configure_mcp_tools(
@@ -323,7 +336,7 @@ impl ManageProfilesModal {
             tool_picker,
             _subscription: dismiss_subscription,
         };
-        self.focus_handle(cx).focus(window);
+        self.focus_handle(cx).focus(window, cx);
     }
 
     fn configure_builtin_tools(
@@ -364,7 +377,7 @@ impl ManageProfilesModal {
             tool_picker,
             _subscription: dismiss_subscription,
         };
-        self.focus_handle(cx).focus(window);
+        self.focus_handle(cx).focus(window, cx);
     }
 
     fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -938,7 +951,7 @@ impl Render for ManageProfilesModal {
             .on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
             .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
             .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
-                this.focus_handle(cx).focus(window);
+                this.focus_handle(cx).focus(window, cx);
             }))
             .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
             .child(match &self.mode {

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

@@ -212,10 +212,10 @@ impl AgentDiffPane {
                 .focus_handle(cx)
                 .contains_focused(window, cx)
         {
-            self.focus_handle.focus(window);
+            self.focus_handle.focus(window, cx);
         } else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
             self.editor.update(cx, |editor, cx| {
-                editor.focus_handle(cx).focus(window);
+                editor.focus_handle(cx).focus(window, cx);
             });
         }
     }
@@ -874,12 +874,12 @@ impl AgentDiffToolbar {
         match active_item {
             AgentDiffToolbarItem::Pane(agent_diff) => {
                 if let Some(agent_diff) = agent_diff.upgrade() {
-                    agent_diff.focus_handle(cx).focus(window);
+                    agent_diff.focus_handle(cx).focus(window, cx);
                 }
             }
             AgentDiffToolbarItem::Editor { editor, .. } => {
                 if let Some(editor) = editor.upgrade() {
-                    editor.read(cx).focus_handle(cx).focus(window);
+                    editor.read(cx).focus_handle(cx).focus(window, cx);
                 }
             }
         }

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

@@ -29,26 +29,39 @@ impl AgentModelSelector {
 
         Self {
             selector: cx.new(move |cx| {
-                let fs = fs.clone();
                 language_model_selector(
                     {
                         let model_context = model_usage_context.clone();
                         move |cx| model_context.configured_model(cx)
                     },
-                    move |model, cx| {
-                        let provider = model.provider_id().0.to_string();
-                        let model_id = model.id().0.to_string();
-                        match &model_usage_context {
-                            ModelUsageContext::InlineAssistant => {
-                                update_settings_file(fs.clone(), cx, move |settings, _cx| {
-                                    settings
-                                        .agent
-                                        .get_or_insert_default()
-                                        .set_inline_assistant_model(provider.clone(), model_id);
-                                });
+                    {
+                        let fs = fs.clone();
+                        move |model, cx| {
+                            let provider = model.provider_id().0.to_string();
+                            let model_id = model.id().0.to_string();
+                            match &model_usage_context {
+                                ModelUsageContext::InlineAssistant => {
+                                    update_settings_file(fs.clone(), cx, move |settings, _cx| {
+                                        settings
+                                            .agent
+                                            .get_or_insert_default()
+                                            .set_inline_assistant_model(provider.clone(), model_id);
+                                    });
+                                }
                             }
                         }
                     },
+                    {
+                        let fs = fs.clone();
+                        move |model, should_be_favorite, cx| {
+                            crate::favorite_models::toggle_in_settings(
+                                model,
+                                should_be_favorite,
+                                fs.clone(),
+                                cx,
+                            );
+                        }
+                    },
                     true, // Use popover styles for picker
                     focus_handle_clone,
                     window,

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

@@ -7,7 +7,6 @@ use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use project::{
     ExternalAgentServerName,
     agent_server_store::{CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
-    trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, wait_for_workspace_trust},
 };
 use serde::{Deserialize, Serialize};
 use settings::{
@@ -264,17 +263,6 @@ impl AgentType {
             Self::Custom { .. } => Some(IconName::Sparkle),
         }
     }
-
-    fn is_mcp(&self) -> bool {
-        match self {
-            Self::NativeAgent => false,
-            Self::TextThread => false,
-            Self::Custom { .. } => false,
-            Self::Gemini => true,
-            Self::ClaudeCode => true,
-            Self::Codex => true,
-        }
-    }
 }
 
 impl From<ExternalAgent> for AgentType {
@@ -455,9 +443,7 @@ pub struct AgentPanel {
     pending_serialization: Option<Task<Result<()>>>,
     onboarding: Entity<AgentPanelOnboarding>,
     selected_agent: AgentType,
-    new_agent_thread_task: Task<()>,
     show_trust_workspace_message: bool,
-    _worktree_trust_subscription: Option<Subscription>,
 }
 
 impl AgentPanel {
@@ -681,48 +667,6 @@ impl AgentPanel {
             None
         };
 
-        let mut show_trust_workspace_message = false;
-        let worktree_trust_subscription =
-            TrustedWorktrees::try_get_global(cx).and_then(|trusted_worktrees| {
-                let has_global_trust = trusted_worktrees.update(cx, |trusted_worktrees, cx| {
-                    trusted_worktrees.can_trust_workspace(
-                        project
-                            .read(cx)
-                            .remote_connection_options(cx)
-                            .map(RemoteHostLocation::from),
-                        cx,
-                    )
-                });
-                if has_global_trust {
-                    None
-                } else {
-                    show_trust_workspace_message = true;
-                    let project = project.clone();
-                    Some(cx.subscribe(
-                        &trusted_worktrees,
-                        move |agent_panel, trusted_worktrees, _, cx| {
-                            let new_show_trust_workspace_message =
-                                !trusted_worktrees.update(cx, |trusted_worktrees, cx| {
-                                    trusted_worktrees.can_trust_workspace(
-                                        project
-                                            .read(cx)
-                                            .remote_connection_options(cx)
-                                            .map(RemoteHostLocation::from),
-                                        cx,
-                                    )
-                                });
-                            if new_show_trust_workspace_message
-                                != agent_panel.show_trust_workspace_message
-                            {
-                                agent_panel.show_trust_workspace_message =
-                                    new_show_trust_workspace_message;
-                                cx.notify();
-                            };
-                        },
-                    ))
-                }
-            });
-
         let mut panel = Self {
             active_view,
             workspace,
@@ -745,14 +689,12 @@ impl AgentPanel {
             height: None,
             zoomed: false,
             pending_serialization: None,
-            new_agent_thread_task: Task::ready(()),
             onboarding,
             acp_history,
             history_store,
             selected_agent: AgentType::default(),
             loading: false,
-            show_trust_workspace_message,
-            _worktree_trust_subscription: worktree_trust_subscription,
+            show_trust_workspace_message: false,
         };
 
         // Initial sync of agent servers from extensions
@@ -880,7 +822,7 @@ impl AgentPanel {
             window,
             cx,
         );
-        text_thread_editor.focus_handle(cx).focus(window);
+        text_thread_editor.focus_handle(cx).focus(window, cx);
     }
 
     fn external_thread(
@@ -945,47 +887,6 @@ impl AgentPanel {
                 }
             };
 
-            if ext_agent.is_mcp() {
-                let wait_task = this.update(cx, |agent_panel, cx| {
-                    agent_panel.project.update(cx, |project, cx| {
-                        wait_for_workspace_trust(
-                            project.remote_connection_options(cx),
-                            "context servers",
-                            cx,
-                        )
-                    })
-                })?;
-                if let Some(wait_task) = wait_task {
-                    this.update_in(cx, |agent_panel, window, cx| {
-                        agent_panel.show_trust_workspace_message = true;
-                        cx.notify();
-                        agent_panel.new_agent_thread_task =
-                            cx.spawn_in(window, async move |agent_panel, cx| {
-                                wait_task.await;
-                                let server = ext_agent.server(fs, history);
-                                agent_panel
-                                    .update_in(cx, |agent_panel, window, cx| {
-                                        agent_panel.show_trust_workspace_message = false;
-                                        cx.notify();
-                                        agent_panel._external_thread(
-                                            server,
-                                            resume_thread,
-                                            summarize_thread,
-                                            workspace,
-                                            project,
-                                            loading,
-                                            ext_agent,
-                                            window,
-                                            cx,
-                                        );
-                                    })
-                                    .ok();
-                            });
-                    })?;
-                    return Ok(());
-                }
-            }
-
             let server = ext_agent.server(fs, history);
             this.update_in(cx, |agent_panel, window, cx| {
                 agent_panel._external_thread(
@@ -1034,7 +935,7 @@ impl AgentPanel {
         if let Some(thread_view) = self.active_thread_view() {
             thread_view.update(cx, |view, cx| {
                 view.expand_message_editor(&ExpandMessageEditor, window, cx);
-                view.focus_handle(cx).focus(window);
+                view.focus_handle(cx).focus(window, cx);
             });
         }
     }
@@ -1115,12 +1016,12 @@ impl AgentPanel {
 
                     match &self.active_view {
                         ActiveView::ExternalAgentThread { thread_view } => {
-                            thread_view.focus_handle(cx).focus(window);
+                            thread_view.focus_handle(cx).focus(window, cx);
                         }
                         ActiveView::TextThread {
                             text_thread_editor, ..
                         } => {
-                            text_thread_editor.focus_handle(cx).focus(window);
+                            text_thread_editor.focus_handle(cx).focus(window, cx);
                         }
                         ActiveView::History | ActiveView::Configuration => {}
                     }
@@ -1268,7 +1169,7 @@ impl AgentPanel {
                 Self::handle_agent_configuration_event,
             ));
 
-            configuration.focus_handle(cx).focus(window);
+            configuration.focus_handle(cx).focus(window, cx);
         }
     }
 
@@ -1404,7 +1305,7 @@ impl AgentPanel {
         }
 
         if focus {
-            self.focus_handle(cx).focus(window);
+            self.focus_handle(cx).focus(window, cx);
         }
     }
 
@@ -1510,36 +1411,6 @@ impl AgentPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let wait_task = if agent.is_mcp() {
-            self.project.update(cx, |project, cx| {
-                wait_for_workspace_trust(
-                    project.remote_connection_options(cx),
-                    "context servers",
-                    cx,
-                )
-            })
-        } else {
-            None
-        };
-        if let Some(wait_task) = wait_task {
-            self.show_trust_workspace_message = true;
-            cx.notify();
-            self.new_agent_thread_task = cx.spawn_in(window, async move |agent_panel, cx| {
-                wait_task.await;
-                agent_panel
-                    .update_in(cx, |agent_panel, window, cx| {
-                        agent_panel.show_trust_workspace_message = false;
-                        cx.notify();
-                        agent_panel._new_agent_thread(agent, window, cx);
-                    })
-                    .ok();
-            });
-        } else {
-            self._new_agent_thread(agent, window, cx);
-        }
-    }
-
-    fn _new_agent_thread(&mut self, agent: AgentType, window: &mut Window, cx: &mut Context<Self>) {
         match agent {
             AgentType::TextThread => {
                 window.dispatch_action(NewTextThread.boxed_clone(), cx);
@@ -1749,14 +1620,19 @@ impl AgentPanel {
 
         let content = match &self.active_view {
             ActiveView::ExternalAgentThread { thread_view } => {
+                let is_generating_title = thread_view
+                    .read(cx)
+                    .as_native_thread(cx)
+                    .map_or(false, |t| t.read(cx).is_generating_title());
+
                 if let Some(title_editor) = thread_view.read(cx).title_editor() {
-                    div()
+                    let container = div()
                         .w_full()
                         .on_action({
                             let thread_view = thread_view.downgrade();
                             move |_: &menu::Confirm, window, cx| {
                                 if let Some(thread_view) = thread_view.upgrade() {
-                                    thread_view.focus_handle(cx).focus(window);
+                                    thread_view.focus_handle(cx).focus(window, cx);
                                 }
                             }
                         })
@@ -1764,12 +1640,25 @@ impl AgentPanel {
                             let thread_view = thread_view.downgrade();
                             move |_: &editor::actions::Cancel, window, cx| {
                                 if let Some(thread_view) = thread_view.upgrade() {
-                                    thread_view.focus_handle(cx).focus(window);
+                                    thread_view.focus_handle(cx).focus(window, cx);
                                 }
                             }
                         })
-                        .child(title_editor)
-                        .into_any_element()
+                        .child(title_editor);
+
+                    if is_generating_title {
+                        container
+                            .with_animation(
+                                "generating_title",
+                                Animation::new(Duration::from_secs(2))
+                                    .repeat()
+                                    .with_easing(pulsating_between(0.4, 0.8)),
+                                |div, delta| div.opacity(delta),
+                            )
+                            .into_any_element()
+                    } else {
+                        container.into_any_element()
+                    }
                 } else {
                     Label::new(thread_view.read(cx).title(cx))
                         .color(Color::Muted)
@@ -1799,6 +1688,13 @@ impl AgentPanel {
                             Label::new(LOADING_SUMMARY_PLACEHOLDER)
                                 .truncate()
                                 .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()
                         }
                     }
@@ -1842,6 +1738,25 @@ impl AgentPanel {
             .into_any()
     }
 
+    fn handle_regenerate_thread_title(thread_view: Entity<AcpThreadView>, cx: &mut App) {
+        thread_view.update(cx, |thread_view, cx| {
+            if let Some(thread) = thread_view.as_native_thread(cx) {
+                thread.update(cx, |thread, cx| {
+                    thread.generate_title(cx);
+                });
+            }
+        });
+    }
+
+    fn handle_regenerate_text_thread_title(
+        text_thread_editor: Entity<TextThreadEditor>,
+        cx: &mut App,
+    ) {
+        text_thread_editor.update(cx, |text_thread_editor, cx| {
+            text_thread_editor.regenerate_summary(cx);
+        });
+    }
+
     fn render_panel_options_menu(
         &self,
         window: &mut Window,
@@ -1861,6 +1776,35 @@ impl AgentPanel {
 
         let selected_agent = self.selected_agent.clone();
 
+        let text_thread_view = match &self.active_view {
+            ActiveView::TextThread {
+                text_thread_editor, ..
+            } => Some(text_thread_editor.clone()),
+            _ => None,
+        };
+        let text_thread_with_messages = match &self.active_view {
+            ActiveView::TextThread {
+                text_thread_editor, ..
+            } => text_thread_editor
+                .read(cx)
+                .text_thread()
+                .read(cx)
+                .messages(cx)
+                .any(|message| message.role == language_model::Role::Assistant),
+            _ => false,
+        };
+
+        let thread_view = match &self.active_view {
+            ActiveView::ExternalAgentThread { thread_view } => Some(thread_view.clone()),
+            _ => None,
+        };
+        let thread_with_messages = match &self.active_view {
+            ActiveView::ExternalAgentThread { thread_view } => {
+                thread_view.read(cx).has_user_submitted_prompt(cx)
+            }
+            _ => false,
+        };
+
         PopoverMenu::new("agent-options-menu")
             .trigger_with_tooltip(
                 IconButton::new("agent-options-menu", IconName::Ellipsis)
@@ -1883,6 +1827,7 @@ impl AgentPanel {
                 move |window, cx| {
                     Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
                         menu = menu.context(focus_handle.clone());
+
                         if let Some(usage) = usage {
                             menu = menu
                                 .header_with_link("Prompt Usage", "Manage", account_url.clone())
@@ -1920,6 +1865,38 @@ impl AgentPanel {
                                 .separator()
                         }
 
+                        if thread_with_messages | text_thread_with_messages {
+                            menu = menu.header("Current Thread");
+
+                            if let Some(text_thread_view) = text_thread_view.as_ref() {
+                                menu = menu
+                                    .entry("Regenerate Thread Title", None, {
+                                        let text_thread_view = text_thread_view.clone();
+                                        move |_, cx| {
+                                            Self::handle_regenerate_text_thread_title(
+                                                text_thread_view.clone(),
+                                                cx,
+                                            );
+                                        }
+                                    })
+                                    .separator();
+                            }
+
+                            if let Some(thread_view) = thread_view.as_ref() {
+                                menu = menu
+                                    .entry("Regenerate Thread Title", None, {
+                                        let thread_view = thread_view.clone();
+                                        move |_, cx| {
+                                            Self::handle_regenerate_thread_title(
+                                                thread_view.clone(),
+                                                cx,
+                                            );
+                                        }
+                                    })
+                                    .separator();
+                            }
+                        }
+
                         menu = menu
                             .header("MCP Servers")
                             .action(

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

@@ -7,6 +7,7 @@ mod buffer_codegen;
 mod completion_provider;
 mod context;
 mod context_server_configuration;
+mod favorite_models;
 mod inline_assistant;
 mod inline_prompt_editor;
 mod language_model_selector;
@@ -67,6 +68,8 @@ actions!(
         ToggleProfileSelector,
         /// Cycles through available session modes.
         CycleModeSelector,
+        /// Cycles through favorited models in the ACP model selector.
+        CycleFavoriteModels,
         /// Expands the message editor to full size.
         ExpandMessageEditor,
         /// Removes all thread history.
@@ -171,16 +174,6 @@ impl ExternalAgent {
             Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
         }
     }
-
-    pub fn is_mcp(&self) -> bool {
-        match self {
-            Self::Gemini => true,
-            Self::ClaudeCode => true,
-            Self::Codex => true,
-            Self::NativeAgent => false,
-            Self::Custom { .. } => false,
-        }
-    }
 }
 
 /// Opens the profile management interface for configuring agent tools and settings.
@@ -467,6 +460,7 @@ mod tests {
             commit_message_model: None,
             thread_summary_model: None,
             inline_alternatives: vec![],
+            favorite_models: vec![],
             default_profile: AgentProfileId::default(),
             default_view: DefaultAgentView::Thread,
             profiles: Default::default(),

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

@@ -75,6 +75,9 @@ pub struct BufferCodegen {
     session_id: Uuid,
 }
 
+pub const REWRITE_SECTION_TOOL_NAME: &str = "rewrite_section";
+pub const FAILURE_MESSAGE_TOOL_NAME: &str = "failure_message";
+
 impl BufferCodegen {
     pub fn new(
         buffer: Entity<MultiBuffer>,
@@ -441,7 +444,8 @@ impl CodegenAlternative {
                     })
                     .boxed_local()
                 };
-            self.generation = self.handle_stream(model, stream, cx);
+            self.generation =
+                self.handle_stream(model, /* strip_invalid_spans: */ true, stream, cx);
         }
 
         Ok(())
@@ -521,12 +525,12 @@ impl CodegenAlternative {
 
             let tools = vec![
                 LanguageModelRequestTool {
-                    name: "rewrite_section".to_string(),
+                    name: REWRITE_SECTION_TOOL_NAME.to_string(),
                     description: "Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
                     input_schema: language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
                 },
                 LanguageModelRequestTool {
-                    name: "failure_message".to_string(),
+                    name: FAILURE_MESSAGE_TOOL_NAME.to_string(),
                     description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
                     input_schema: language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
                 },
@@ -629,6 +633,7 @@ impl CodegenAlternative {
     pub fn handle_stream(
         &mut self,
         model: Arc<dyn LanguageModel>,
+        strip_invalid_spans: bool,
         stream: impl 'static + Future<Output = Result<LanguageModelTextStream>>,
         cx: &mut Context<Self>,
     ) -> Task<()> {
@@ -713,10 +718,16 @@ impl CodegenAlternative {
                         let mut response_latency = None;
                         let request_start = Instant::now();
                         let diff = async {
-                            let chunks = StripInvalidSpans::new(
-                                stream?.stream.map_err(|error| error.into()),
-                            );
-                            futures::pin_mut!(chunks);
+                            let raw_stream = stream?.stream.map_err(|error| error.into());
+
+                            let stripped;
+                            let mut chunks: Pin<Box<dyn Stream<Item = Result<String>> + Send>> =
+                                if strip_invalid_spans {
+                                    stripped = StripInvalidSpans::new(raw_stream);
+                                    Box::pin(stripped)
+                                } else {
+                                    Box::pin(raw_stream)
+                                };
 
                             let mut diff = StreamingDiff::new(selected_text.to_string());
                             let mut line_diff = LineDiff::default();
@@ -1159,7 +1170,7 @@ impl CodegenAlternative {
             let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option<ToolUseOutput> {
                 let mut chars_read_so_far = chars_read_so_far.lock();
                 match tool_use.name.as_ref() {
-                    "rewrite_section" => {
+                    REWRITE_SECTION_TOOL_NAME => {
                         let Ok(input) =
                             serde_json::from_value::<RewriteSectionInput>(tool_use.input)
                         else {
@@ -1172,7 +1183,7 @@ impl CodegenAlternative {
                             description: None,
                         })
                     }
-                    "failure_message" => {
+                    FAILURE_MESSAGE_TOOL_NAME => {
                         let Ok(mut input) =
                             serde_json::from_value::<FailureMessageInput>(tool_use.input)
                         else {
@@ -1307,7 +1318,12 @@ impl CodegenAlternative {
 
             let Some(task) = codegen
                 .update(cx, move |codegen, cx| {
-                    codegen.handle_stream(model, async { Ok(language_model_text_stream) }, cx)
+                    codegen.handle_stream(
+                        model,
+                        /* strip_invalid_spans: */ false,
+                        async { Ok(language_model_text_stream) },
+                        cx,
+                    )
                 })
                 .ok()
             else {
@@ -1480,7 +1496,10 @@ mod tests {
     use indoc::indoc;
     use language::{Buffer, Point};
     use language_model::fake_provider::FakeLanguageModel;
-    use language_model::{LanguageModelRegistry, TokenUsage};
+    use language_model::{
+        LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRegistry,
+        LanguageModelToolUse, StopReason, TokenUsage,
+    };
     use languages::rust_lang;
     use rand::prelude::*;
     use settings::SettingsStore;
@@ -1792,6 +1811,51 @@ mod tests {
         );
     }
 
+    // When not streaming tool calls, we strip backticks as part of parsing the model's
+    // plain text response. This is a regression test for a bug where we stripped
+    // backticks incorrectly.
+    #[gpui::test]
+    async fn test_allows_model_to_output_backticks(cx: &mut TestAppContext) {
+        init_test(cx);
+        let text = "- Improved; `cmd+click` behavior. Now requires `cmd` to be pressed before the click starts or it doesn't run. ([#44579](https://github.com/zed-industries/zed/pull/44579); thanks [Zachiah](https://github.com/Zachiah))";
+        let buffer = cx.new(|cx| Buffer::local("", cx));
+        let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+        let range = buffer.read_with(cx, |buffer, cx| {
+            let snapshot = buffer.snapshot(cx);
+            snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(0, 0))
+        });
+        let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
+        let codegen = cx.new(|cx| {
+            CodegenAlternative::new(
+                buffer.clone(),
+                range.clone(),
+                true,
+                prompt_builder,
+                Uuid::new_v4(),
+                cx,
+            )
+        });
+
+        let events_tx = simulate_tool_based_completion(&codegen, cx);
+        let chunk_len = text.find('`').unwrap();
+        events_tx
+            .unbounded_send(rewrite_tool_use("tool_1", &text[..chunk_len], false))
+            .unwrap();
+        events_tx
+            .unbounded_send(rewrite_tool_use("tool_2", &text, true))
+            .unwrap();
+        events_tx
+            .unbounded_send(LanguageModelCompletionEvent::Stop(StopReason::EndTurn))
+            .unwrap();
+        drop(events_tx);
+        cx.run_until_parked();
+
+        assert_eq!(
+            buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
+            text
+        );
+    }
+
     #[gpui::test]
     async fn test_strip_invalid_spans_from_codeblock() {
         assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await;
@@ -1846,6 +1910,7 @@ mod tests {
         codegen.update(cx, |codegen, cx| {
             codegen.generation = codegen.handle_stream(
                 model,
+                /* strip_invalid_spans: */ false,
                 future::ready(Ok(LanguageModelTextStream {
                     message_id: None,
                     stream: chunks_rx.map(Ok).boxed(),
@@ -1856,4 +1921,39 @@ mod tests {
         });
         chunks_tx
     }
+
+    fn simulate_tool_based_completion(
+        codegen: &Entity<CodegenAlternative>,
+        cx: &mut TestAppContext,
+    ) -> mpsc::UnboundedSender<LanguageModelCompletionEvent> {
+        let (events_tx, events_rx) = mpsc::unbounded();
+        let model = Arc::new(FakeLanguageModel::default());
+        codegen.update(cx, |codegen, cx| {
+            let completion_stream = Task::ready(Ok(events_rx.map(Ok).boxed()
+                as BoxStream<
+                    'static,
+                    Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
+                >));
+            codegen.generation = codegen.handle_completion(model, completion_stream, cx);
+        });
+        events_tx
+    }
+
+    fn rewrite_tool_use(
+        id: &str,
+        replacement_text: &str,
+        is_complete: bool,
+    ) -> LanguageModelCompletionEvent {
+        let input = RewriteSectionInput {
+            replacement_text: replacement_text.into(),
+        };
+        LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
+            id: id.into(),
+            name: REWRITE_SECTION_TOOL_NAME.into(),
+            raw_input: serde_json::to_string(&input).unwrap(),
+            input: serde_json::to_value(&input).unwrap(),
+            is_input_complete: is_complete,
+            thought_signature: None,
+        })
+    }
 }

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

@@ -0,0 +1,57 @@
+use std::sync::Arc;
+
+use agent_client_protocol::ModelId;
+use fs::Fs;
+use language_model::LanguageModel;
+use settings::{LanguageModelSelection, update_settings_file};
+use ui::App;
+
+fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelSelection {
+    LanguageModelSelection {
+        provider: model.provider_id().to_string().into(),
+        model: model.id().0.to_string(),
+    }
+}
+
+fn model_id_to_selection(model_id: &ModelId) -> LanguageModelSelection {
+    let id = model_id.0.as_ref();
+    let (provider, model) = id.split_once('/').unwrap_or(("", id));
+    LanguageModelSelection {
+        provider: provider.to_owned().into(),
+        model: model.to_owned(),
+    }
+}
+
+pub fn toggle_in_settings(
+    model: Arc<dyn LanguageModel>,
+    should_be_favorite: bool,
+    fs: Arc<dyn Fs>,
+    cx: &App,
+) {
+    let selection = language_model_to_selection(&model);
+    update_settings_file(fs, cx, move |settings, _| {
+        let agent = settings.agent.get_or_insert_default();
+        if should_be_favorite {
+            agent.add_favorite_model(selection.clone());
+        } else {
+            agent.remove_favorite_model(&selection);
+        }
+    });
+}
+
+pub fn toggle_model_id_in_settings(
+    model_id: ModelId,
+    should_be_favorite: bool,
+    fs: Arc<dyn Fs>,
+    cx: &App,
+) {
+    let selection = model_id_to_selection(&model_id);
+    update_settings_file(fs, cx, move |settings, _| {
+        let agent = settings.agent.get_or_insert_default();
+        if should_be_favorite {
+            agent.add_favorite_model(selection.clone());
+        } else {
+            agent.remove_favorite_model(&selection);
+        }
+    });
+}

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

@@ -1197,7 +1197,7 @@ impl InlineAssistant {
 
         assist
             .editor
-            .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
+            .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
             .ok();
     }
 
@@ -1209,7 +1209,7 @@ impl InlineAssistant {
         if let Some(decorations) = assist.decorations.as_ref() {
             decorations.prompt_editor.update(cx, |prompt_editor, cx| {
                 prompt_editor.editor.update(cx, |editor, cx| {
-                    window.focus(&editor.focus_handle(cx));
+                    window.focus(&editor.focus_handle(cx), cx);
                     editor.select_all(&SelectAll, window, cx);
                 })
             });
@@ -2271,6 +2271,36 @@ pub mod evals {
         );
     }
 
+    #[test]
+    #[cfg_attr(not(feature = "unit-eval"), ignore)]
+    fn eval_empty_buffer() {
+        run_eval(
+            20,
+            1.0,
+            "Write a Python hello, world program".to_string(),
+            "Λ‡".to_string(),
+            |output| match output {
+                InlineAssistantOutput::Success {
+                    full_buffer_text, ..
+                } => {
+                    if full_buffer_text.is_empty() {
+                        EvalOutput::failed("expected some output".to_string())
+                    } else {
+                        EvalOutput::passed(format!("Produced {full_buffer_text}"))
+                    }
+                }
+                o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!(
+                    "Assistant output does not match expected output: {:?}",
+                    o
+                )),
+                o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!(
+                    "Assistant output does not match expected output: {:?}",
+                    o
+                )),
+            },
+        );
+    }
+
     fn run_eval(
         iterations: usize,
         expected_pass_ratio: f32,

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

@@ -357,7 +357,7 @@ impl<T: 'static> PromptEditor<T> {
             creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
 
             if focus {
-                window.focus(&editor.focus_handle(cx));
+                window.focus(&editor.focus_handle(cx), cx);
             }
             editor
         });
@@ -844,26 +844,59 @@ impl<T: 'static> PromptEditor<T> {
 
                     if show_rating_buttons {
                         buttons.push(
-                            IconButton::new("thumbs-down", IconName::ThumbsDown)
-                                .icon_color(if rated { Color::Muted } else { Color::Default })
-                                .shape(IconButtonShape::Square)
-                                .disabled(rated)
-                                .tooltip(Tooltip::text("Bad result"))
-                                .on_click(cx.listener(|this, _, window, cx| {
-                                    this.thumbs_down(&ThumbsDownResult, window, cx);
-                                }))
-                                .into_any_element(),
-                        );
-
-                        buttons.push(
-                            IconButton::new("thumbs-up", IconName::ThumbsUp)
-                                .icon_color(if rated { Color::Muted } else { Color::Default })
-                                .shape(IconButtonShape::Square)
-                                .disabled(rated)
-                                .tooltip(Tooltip::text("Good result"))
-                                .on_click(cx.listener(|this, _, window, cx| {
-                                    this.thumbs_up(&ThumbsUpResult, window, cx);
-                                }))
+                            h_flex()
+                                .pl_1()
+                                .gap_1()
+                                .border_l_1()
+                                .border_color(cx.theme().colors().border_variant)
+                                .child(
+                                    IconButton::new("thumbs-up", IconName::ThumbsUp)
+                                        .shape(IconButtonShape::Square)
+                                        .map(|this| {
+                                            if rated {
+                                                this.disabled(true)
+                                                    .icon_color(Color::Ignored)
+                                                    .tooltip(move |_, cx| {
+                                                        Tooltip::with_meta(
+                                                            "Good Result",
+                                                            None,
+                                                            "You already rated this result",
+                                                            cx,
+                                                        )
+                                                    })
+                                            } else {
+                                                this.icon_color(Color::Muted)
+                                                    .tooltip(Tooltip::text("Good Result"))
+                                            }
+                                        })
+                                        .on_click(cx.listener(|this, _, window, cx| {
+                                            this.thumbs_up(&ThumbsUpResult, window, cx);
+                                        })),
+                                )
+                                .child(
+                                    IconButton::new("thumbs-down", IconName::ThumbsDown)
+                                        .shape(IconButtonShape::Square)
+                                        .map(|this| {
+                                            if rated {
+                                                this.disabled(true)
+                                                    .icon_color(Color::Ignored)
+                                                    .tooltip(move |_, cx| {
+                                                        Tooltip::with_meta(
+                                                            "Bad Result",
+                                                            None,
+                                                            "You already rated this result",
+                                                            cx,
+                                                        )
+                                                    })
+                                            } else {
+                                                this.icon_color(Color::Muted)
+                                                    .tooltip(Tooltip::text("Bad Result"))
+                                            }
+                                        })
+                                        .on_click(cx.listener(|this, _, window, cx| {
+                                            this.thumbs_down(&ThumbsDownResult, window, cx);
+                                        })),
+                                )
                                 .into_any_element(),
                         );
                     }
@@ -927,10 +960,21 @@ impl<T: 'static> PromptEditor<T> {
     }
 
     fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
+        let focus_handle = self.editor.focus_handle(cx);
+
         IconButton::new("cancel", IconName::Close)
             .icon_color(Color::Muted)
             .shape(IconButtonShape::Square)
-            .tooltip(Tooltip::text("Close Assistant"))
+            .tooltip({
+                move |_window, cx| {
+                    Tooltip::for_action_in(
+                        "Close Assistant",
+                        &editor::actions::Cancel,
+                        &focus_handle,
+                        cx,
+                    )
+                }
+            })
             .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
             .into_any_element()
     }

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

@@ -1,16 +1,18 @@
 use std::{cmp::Reverse, sync::Arc};
 
-use collections::IndexMap;
+use agent_settings::AgentSettings;
+use collections::{HashMap, HashSet, IndexMap};
 use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
 use gpui::{
     Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
 };
 use language_model::{
-    AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
-    LanguageModelRegistry,
+    AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProvider,
+    LanguageModelProviderId, LanguageModelRegistry,
 };
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate};
+use settings::Settings;
 use ui::prelude::*;
 use zed_actions::agent::OpenSettings;
 
@@ -18,12 +20,14 @@ use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem}
 
 type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
 type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
+type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &App) + 'static>;
 
 pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
 
 pub fn language_model_selector(
     get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
     on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
+    on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
     popover_styles: bool,
     focus_handle: FocusHandle,
     window: &mut Window,
@@ -32,6 +36,7 @@ pub fn language_model_selector(
     let delegate = LanguageModelPickerDelegate::new(
         get_active_model,
         on_model_changed,
+        on_toggle_favorite,
         popover_styles,
         focus_handle,
         window,
@@ -49,7 +54,17 @@ pub fn language_model_selector(
 }
 
 fn all_models(cx: &App) -> GroupedModels {
-    let providers = LanguageModelRegistry::global(cx).read(cx).providers();
+    let lm_registry = LanguageModelRegistry::global(cx).read(cx);
+    let providers = lm_registry.providers();
+
+    let mut favorites_index = FavoritesIndex::default();
+
+    for sel in &AgentSettings::get_global(cx).favorite_models {
+        favorites_index
+            .entry(sel.provider.0.clone().into())
+            .or_default()
+            .insert(sel.model.clone().into());
+    }
 
     let recommended = providers
         .iter()
@@ -57,10 +72,7 @@ fn all_models(cx: &App) -> GroupedModels {
             provider
                 .recommended_models(cx)
                 .into_iter()
-                .map(|model| ModelInfo {
-                    model,
-                    icon: provider.icon(),
-                })
+                .map(|model| ModelInfo::new(&**provider, model, &favorites_index))
         })
         .collect();
 
@@ -70,25 +82,44 @@ fn all_models(cx: &App) -> GroupedModels {
             provider
                 .provided_models(cx)
                 .into_iter()
-                .map(|model| ModelInfo {
-                    model,
-                    icon: provider.icon(),
-                })
+                .map(|model| ModelInfo::new(&**provider, model, &favorites_index))
         })
         .collect();
 
     GroupedModels::new(all, recommended)
 }
 
+type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>;
+
 #[derive(Clone)]
 struct ModelInfo {
     model: Arc<dyn LanguageModel>,
     icon: IconName,
+    is_favorite: bool,
+}
+
+impl ModelInfo {
+    fn new(
+        provider: &dyn LanguageModelProvider,
+        model: Arc<dyn LanguageModel>,
+        favorites_index: &FavoritesIndex,
+    ) -> Self {
+        let is_favorite = favorites_index
+            .get(&provider.id())
+            .map_or(false, |set| set.contains(&model.id()));
+
+        Self {
+            model,
+            icon: provider.icon(),
+            is_favorite,
+        }
+    }
 }
 
 pub struct LanguageModelPickerDelegate {
     on_model_changed: OnModelChanged,
     get_active_model: GetActiveModel,
+    on_toggle_favorite: OnToggleFavorite,
     all_models: Arc<GroupedModels>,
     filtered_entries: Vec<LanguageModelPickerEntry>,
     selected_index: usize,
@@ -102,6 +133,7 @@ impl LanguageModelPickerDelegate {
     fn new(
         get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
         on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
+        on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
         popover_styles: bool,
         focus_handle: FocusHandle,
         window: &mut Window,
@@ -117,6 +149,7 @@ impl LanguageModelPickerDelegate {
             selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
             filtered_entries: entries,
             get_active_model: Arc::new(get_active_model),
+            on_toggle_favorite: Arc::new(on_toggle_favorite),
             _authenticate_all_providers_task: Self::authenticate_all_providers(cx),
             _subscriptions: vec![cx.subscribe_in(
                 &LanguageModelRegistry::global(cx),
@@ -216,15 +249,57 @@ impl LanguageModelPickerDelegate {
     pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
         (self.get_active_model)(cx)
     }
+
+    pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+        if self.all_models.favorites.is_empty() {
+            return;
+        }
+
+        let active_model = (self.get_active_model)(cx);
+        let active_provider_id = active_model.as_ref().map(|m| m.provider.id());
+        let active_model_id = active_model.as_ref().map(|m| m.model.id());
+
+        let current_index = self
+            .all_models
+            .favorites
+            .iter()
+            .position(|info| {
+                Some(info.model.provider_id()) == active_provider_id
+                    && Some(info.model.id()) == active_model_id
+            })
+            .unwrap_or(usize::MAX);
+
+        let next_index = if current_index == usize::MAX {
+            0
+        } else {
+            (current_index + 1) % self.all_models.favorites.len()
+        };
+
+        let next_model = self.all_models.favorites[next_index].model.clone();
+
+        (self.on_model_changed)(next_model, cx);
+
+        // Align the picker selection with the newly-active model
+        let new_index =
+            Self::get_active_model_index(&self.filtered_entries, (self.get_active_model)(cx));
+        self.set_selected_index(new_index, window, cx);
+    }
 }
 
 struct GroupedModels {
+    favorites: Vec<ModelInfo>,
     recommended: Vec<ModelInfo>,
     all: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>,
 }
 
 impl GroupedModels {
     pub fn new(all: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
+        let favorites = all
+            .iter()
+            .filter(|info| info.is_favorite)
+            .cloned()
+            .collect();
+
         let mut all_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
         for model in all {
             let provider = model.model.provider_id();
@@ -236,6 +311,7 @@ impl GroupedModels {
         }
 
         Self {
+            favorites,
             recommended,
             all: all_by_provider,
         }
@@ -244,13 +320,18 @@ impl GroupedModels {
     fn entries(&self) -> Vec<LanguageModelPickerEntry> {
         let mut entries = Vec::new();
 
+        if !self.favorites.is_empty() {
+            entries.push(LanguageModelPickerEntry::Separator("Favorite".into()));
+            for info in &self.favorites {
+                entries.push(LanguageModelPickerEntry::Model(info.clone()));
+            }
+        }
+
         if !self.recommended.is_empty() {
             entries.push(LanguageModelPickerEntry::Separator("Recommended".into()));
-            entries.extend(
-                self.recommended
-                    .iter()
-                    .map(|info| LanguageModelPickerEntry::Model(info.clone())),
-            );
+            for info in &self.recommended {
+                entries.push(LanguageModelPickerEntry::Model(info.clone()));
+            }
         }
 
         for models in self.all.values() {
@@ -260,12 +341,11 @@ impl GroupedModels {
             entries.push(LanguageModelPickerEntry::Separator(
                 models[0].model.provider_name().0,
             ));
-            entries.extend(
-                models
-                    .iter()
-                    .map(|info| LanguageModelPickerEntry::Model(info.clone())),
-            );
+            for info in models {
+                entries.push(LanguageModelPickerEntry::Model(info.clone()));
+            }
         }
+
         entries
     }
 }
@@ -461,7 +541,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
     fn render_match(
         &self,
         ix: usize,
-        is_focused: bool,
+        selected: bool,
         _: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
@@ -477,11 +557,20 @@ impl PickerDelegate for LanguageModelPickerDelegate {
                 let is_selected = Some(model_info.model.provider_id()) == active_provider_id
                     && Some(model_info.model.id()) == active_model_id;
 
+                let is_favorite = model_info.is_favorite;
+                let handle_action_click = {
+                    let model = model_info.model.clone();
+                    let on_toggle_favorite = self.on_toggle_favorite.clone();
+                    move |cx: &App| on_toggle_favorite(model.clone(), !is_favorite, cx)
+                };
+
                 Some(
                     ModelSelectorListItem::new(ix, model_info.model.name().0)
-                        .is_focused(is_focused)
-                        .is_selected(is_selected)
                         .icon(model_info.icon)
+                        .is_selected(is_selected)
+                        .is_focused(selected)
+                        .is_favorite(is_favorite)
+                        .on_toggle_favorite(handle_action_click)
                         .into_any_element(),
                 )
             }
@@ -493,12 +582,12 @@ impl PickerDelegate for LanguageModelPickerDelegate {
         _window: &mut Window,
         _cx: &mut Context<Picker<Self>>,
     ) -> Option<gpui::AnyElement> {
+        let focus_handle = self.focus_handle.clone();
+
         if !self.popover_styles {
             return None;
         }
 
-        let focus_handle = self.focus_handle.clone();
-
         Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
     }
 }
@@ -598,11 +687,24 @@ mod tests {
     }
 
     fn create_models(model_specs: Vec<(&str, &str)>) -> Vec<ModelInfo> {
+        create_models_with_favorites(model_specs, vec![])
+    }
+
+    fn create_models_with_favorites(
+        model_specs: Vec<(&str, &str)>,
+        favorites: Vec<(&str, &str)>,
+    ) -> Vec<ModelInfo> {
         model_specs
             .into_iter()
-            .map(|(provider, name)| ModelInfo {
-                model: Arc::new(TestLanguageModel::new(name, provider)),
-                icon: IconName::Ai,
+            .map(|(provider, name)| {
+                let is_favorite = favorites
+                    .iter()
+                    .any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
+                ModelInfo {
+                    model: Arc::new(TestLanguageModel::new(name, provider)),
+                    icon: IconName::Ai,
+                    is_favorite,
+                }
             })
             .collect()
     }
@@ -740,4 +842,93 @@ mod tests {
             vec!["zed/claude", "zed/gemini", "copilot/claude"],
         );
     }
+
+    #[gpui::test]
+    fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
+        let recommended_models = create_models(vec![("zed", "claude")]);
+        let all_models = create_models_with_favorites(
+            vec![("zed", "claude"), ("zed", "gemini"), ("openai", "gpt-4")],
+            vec![("zed", "gemini")],
+        );
+
+        let grouped_models = GroupedModels::new(all_models, recommended_models);
+        let entries = grouped_models.entries();
+
+        assert!(matches!(
+            entries.first(),
+            Some(LanguageModelPickerEntry::Separator(s)) if s == "Favorite"
+        ));
+
+        assert_models_eq(grouped_models.favorites, vec!["zed/gemini"]);
+    }
+
+    #[gpui::test]
+    fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
+        let recommended_models = create_models(vec![("zed", "claude")]);
+        let all_models = create_models(vec![("zed", "claude"), ("zed", "gemini")]);
+
+        let grouped_models = GroupedModels::new(all_models, recommended_models);
+        let entries = grouped_models.entries();
+
+        assert!(matches!(
+            entries.first(),
+            Some(LanguageModelPickerEntry::Separator(s)) if s == "Recommended"
+        ));
+
+        assert!(grouped_models.favorites.is_empty());
+    }
+
+    #[gpui::test]
+    fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
+        let recommended_models =
+            create_models_with_favorites(vec![("zed", "claude")], vec![("zed", "claude")]);
+        let all_models = create_models_with_favorites(
+            vec![("zed", "claude"), ("zed", "gemini"), ("openai", "gpt-4")],
+            vec![("zed", "claude")],
+        );
+
+        let grouped_models = GroupedModels::new(all_models, recommended_models);
+        let entries = grouped_models.entries();
+
+        for entry in &entries {
+            if let LanguageModelPickerEntry::Model(info) = entry {
+                if info.model.telemetry_id() == "zed/claude" {
+                    assert!(info.is_favorite, "zed/claude should be a favorite");
+                } else {
+                    assert!(
+                        !info.is_favorite,
+                        "{} should not be a favorite",
+                        info.model.telemetry_id()
+                    );
+                }
+            }
+        }
+    }
+
+    #[gpui::test]
+    fn test_favorites_appear_in_other_sections(_cx: &mut TestAppContext) {
+        let favorites = vec![("zed", "gemini"), ("openai", "gpt-4")];
+
+        let recommended_models =
+            create_models_with_favorites(vec![("zed", "claude")], favorites.clone());
+
+        let all_models = create_models_with_favorites(
+            vec![
+                ("zed", "claude"),
+                ("zed", "gemini"),
+                ("openai", "gpt-4"),
+                ("openai", "gpt-3.5"),
+            ],
+            favorites,
+        );
+
+        let grouped_models = GroupedModels::new(all_models, recommended_models);
+
+        assert_models_eq(grouped_models.favorites, vec!["zed/gemini", "openai/gpt-4"]);
+        assert_models_eq(grouped_models.recommended, vec!["zed/claude"]);
+        assert_models_eq(
+            grouped_models.all.values().flatten().cloned().collect(),
+            vec!["zed/claude", "zed/gemini", "openai/gpt-4", "openai/gpt-3.5"],
+        );
+    }
 }

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

@@ -127,7 +127,7 @@ impl TerminalInlineAssistant {
         if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
             prompt_editor.update(cx, |this, cx| {
                 this.editor.update(cx, |editor, cx| {
-                    window.focus(&editor.focus_handle(cx));
+                    window.focus(&editor.focus_handle(cx), cx);
                     editor.select_all(&SelectAll, window, cx);
                 });
             });
@@ -292,7 +292,7 @@ impl TerminalInlineAssistant {
                 .terminal
                 .update(cx, |this, cx| {
                     this.clear_block_below_cursor(cx);
-                    this.focus_handle(cx).focus(window);
+                    this.focus_handle(cx).focus(window, cx);
                 })
                 .log_err();
 
@@ -369,7 +369,7 @@ impl TerminalInlineAssistant {
             .terminal
             .update(cx, |this, cx| {
                 this.clear_block_below_cursor(cx);
-                this.focus_handle(cx).focus(window);
+                this.focus_handle(cx).focus(window, cx);
             })
             .is_ok()
     }

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

@@ -2,7 +2,7 @@ use crate::{
     language_model_selector::{LanguageModelSelector, language_model_selector},
     ui::BurnModeTooltip,
 };
-use agent_settings::CompletionMode;
+use agent_settings::{AgentSettings, CompletionMode};
 use anyhow::Result;
 use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
 use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
@@ -71,7 +71,9 @@ use workspace::{
     pane,
     searchable::{SearchEvent, SearchableItem},
 };
-use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
+use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
+
+use crate::CycleFavoriteModels;
 
 use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
 use assistant_text_thread::{
@@ -304,17 +306,31 @@ impl TextThreadEditor {
             language_model_selector: cx.new(|cx| {
                 language_model_selector(
                     |cx| LanguageModelRegistry::read_global(cx).default_model(),
-                    move |model, cx| {
-                        update_settings_file(fs.clone(), cx, move |settings, _| {
-                            let provider = model.provider_id().0.to_string();
-                            let model = model.id().0.to_string();
-                            settings.agent.get_or_insert_default().set_model(
-                                LanguageModelSelection {
-                                    provider: LanguageModelProviderSetting(provider),
-                                    model,
-                                },
-                            )
-                        });
+                    {
+                        let fs = fs.clone();
+                        move |model, cx| {
+                            update_settings_file(fs.clone(), cx, move |settings, _| {
+                                let provider = model.provider_id().0.to_string();
+                                let model = model.id().0.to_string();
+                                settings.agent.get_or_insert_default().set_model(
+                                    LanguageModelSelection {
+                                        provider: LanguageModelProviderSetting(provider),
+                                        model,
+                                    },
+                                )
+                            });
+                        }
+                    },
+                    {
+                        let fs = fs.clone();
+                        move |model, should_be_favorite, cx| {
+                            crate::favorite_models::toggle_in_settings(
+                                model,
+                                should_be_favorite,
+                                fs.clone(),
+                                cx,
+                            );
+                        }
                     },
                     true, // Use popover styles for picker
                     focus_handle,
@@ -1325,7 +1341,7 @@ impl TextThreadEditor {
         if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
             active_editor_view.update(cx, |editor, cx| {
                 editor.insert(&text, window, cx);
-                editor.focus_handle(cx).focus(window);
+                editor.focus_handle(cx).focus(window, cx);
             })
         }
     }
@@ -1682,6 +1698,9 @@ impl TextThreadEditor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return;
+        };
         let editor_clipboard_selections = cx
             .read_from_clipboard()
             .and_then(|item| item.entries().first().cloned())
@@ -1692,84 +1711,101 @@ impl TextThreadEditor {
                 _ => None,
             });
 
-        let has_file_context = editor_clipboard_selections
-            .as_ref()
-            .is_some_and(|selections| {
-                selections
-                    .iter()
-                    .any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
-            });
-
-        if has_file_context {
-            if let Some(clipboard_item) = cx.read_from_clipboard() {
-                if let Some(ClipboardEntry::String(clipboard_text)) =
-                    clipboard_item.entries().first()
-                {
-                    if let Some(selections) = editor_clipboard_selections {
-                        cx.stop_propagation();
-
-                        let text = clipboard_text.text();
-                        self.editor.update(cx, |editor, cx| {
-                            let mut current_offset = 0;
-                            let weak_editor = cx.entity().downgrade();
-
-                            for selection in selections {
-                                if let (Some(file_path), Some(line_range)) =
-                                    (selection.file_path, selection.line_range)
-                                {
-                                    let selected_text =
-                                        &text[current_offset..current_offset + selection.len];
-                                    let fence = assistant_slash_commands::codeblock_fence_for_path(
-                                        file_path.to_str(),
-                                        Some(line_range.clone()),
-                                    );
-                                    let formatted_text = format!("{fence}{selected_text}\n```");
-
-                                    let insert_point = editor
-                                        .selections
-                                        .newest::<Point>(&editor.display_snapshot(cx))
-                                        .head();
-                                    let start_row = MultiBufferRow(insert_point.row);
-
-                                    editor.insert(&formatted_text, window, cx);
+        // Insert creases for pasted clipboard selections that:
+        // 1. Contain exactly one selection
+        // 2. Have an associated file path
+        // 3. Span multiple lines (not single-line selections)
+        // 4. Belong to a file that exists in the current project
+        let should_insert_creases = util::maybe!({
+            let selections = editor_clipboard_selections.as_ref()?;
+            if selections.len() > 1 {
+                return Some(false);
+            }
+            let selection = selections.first()?;
+            let file_path = selection.file_path.as_ref()?;
+            let line_range = selection.line_range.as_ref()?;
 
-                                    let snapshot = editor.buffer().read(cx).snapshot(cx);
-                                    let anchor_before = snapshot.anchor_after(insert_point);
-                                    let anchor_after = editor
-                                        .selections
-                                        .newest_anchor()
-                                        .head()
-                                        .bias_left(&snapshot);
+            if line_range.start() == line_range.end() {
+                return Some(false);
+            }
 
-                                    editor.insert("\n", window, cx);
+            Some(
+                workspace
+                    .read(cx)
+                    .project()
+                    .read(cx)
+                    .project_path_for_absolute_path(file_path, cx)
+                    .is_some(),
+            )
+        })
+        .unwrap_or(false);
 
-                                    let crease_text = acp_thread::selection_name(
-                                        Some(file_path.as_ref()),
-                                        &line_range,
-                                    );
+        if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
+            if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
+                if let Some(selections) = editor_clipboard_selections {
+                    cx.stop_propagation();
 
-                                    let fold_placeholder = quote_selection_fold_placeholder(
-                                        crease_text,
-                                        weak_editor.clone(),
-                                    );
-                                    let crease = Crease::inline(
-                                        anchor_before..anchor_after,
-                                        fold_placeholder,
-                                        render_quote_selection_output_toggle,
-                                        |_, _, _, _| Empty.into_any(),
-                                    );
-                                    editor.insert_creases(vec![crease], cx);
-                                    editor.fold_at(start_row, window, cx);
+                    let text = clipboard_text.text();
+                    self.editor.update(cx, |editor, cx| {
+                        let mut current_offset = 0;
+                        let weak_editor = cx.entity().downgrade();
 
-                                    current_offset += selection.len;
-                                    if !selection.is_entire_line && current_offset < text.len() {
-                                        current_offset += 1;
-                                    }
+                        for selection in selections {
+                            if let (Some(file_path), Some(line_range)) =
+                                (selection.file_path, selection.line_range)
+                            {
+                                let selected_text =
+                                    &text[current_offset..current_offset + selection.len];
+                                let fence = assistant_slash_commands::codeblock_fence_for_path(
+                                    file_path.to_str(),
+                                    Some(line_range.clone()),
+                                );
+                                let formatted_text = format!("{fence}{selected_text}\n```");
+
+                                let insert_point = editor
+                                    .selections
+                                    .newest::<Point>(&editor.display_snapshot(cx))
+                                    .head();
+                                let start_row = MultiBufferRow(insert_point.row);
+
+                                editor.insert(&formatted_text, window, cx);
+
+                                let snapshot = editor.buffer().read(cx).snapshot(cx);
+                                let anchor_before = snapshot.anchor_after(insert_point);
+                                let anchor_after = editor
+                                    .selections
+                                    .newest_anchor()
+                                    .head()
+                                    .bias_left(&snapshot);
+
+                                editor.insert("\n", window, cx);
+
+                                let crease_text = acp_thread::selection_name(
+                                    Some(file_path.as_ref()),
+                                    &line_range,
+                                );
+
+                                let fold_placeholder = quote_selection_fold_placeholder(
+                                    crease_text,
+                                    weak_editor.clone(),
+                                );
+                                let crease = Crease::inline(
+                                    anchor_before..anchor_after,
+                                    fold_placeholder,
+                                    render_quote_selection_output_toggle,
+                                    |_, _, _, _| Empty.into_any(),
+                                );
+                                editor.insert_creases(vec![crease], cx);
+                                editor.fold_at(start_row, window, cx);
+
+                                current_offset += selection.len;
+                                if !selection.is_entire_line && current_offset < text.len() {
+                                    current_offset += 1;
                                 }
                             }
-                        });
-                        return;
-                    }
+                        }
+                    });
+                    return;
                 }
             }
         }
@@ -1928,6 +1964,12 @@ impl TextThreadEditor {
         }
     }
 
+    fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
+        self.editor.update(cx, |editor, cx| {
+            editor.paste(&editor::actions::Paste, window, cx);
+        });
+    }
+
     fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
         self.editor.update(cx, |editor, cx| {
             let buffer = editor.buffer().read(cx).snapshot(cx);
@@ -2195,12 +2237,53 @@ impl TextThreadEditor {
         };
 
         let focus_handle = self.editor().focus_handle(cx);
+
         let (color, icon) = if self.language_model_selector_menu_handle.is_deployed() {
             (Color::Accent, IconName::ChevronUp)
         } else {
             (Color::Muted, IconName::ChevronDown)
         };
 
+        let tooltip = Tooltip::element({
+            move |_, cx| {
+                let focus_handle = focus_handle.clone();
+                let should_show_cycle_row = !AgentSettings::get_global(cx)
+                    .favorite_model_ids()
+                    .is_empty();
+
+                v_flex()
+                    .gap_1()
+                    .child(
+                        h_flex()
+                            .gap_2()
+                            .justify_between()
+                            .child(Label::new("Change Model"))
+                            .child(KeyBinding::for_action_in(
+                                &ToggleModelSelector,
+                                &focus_handle,
+                                cx,
+                            )),
+                    )
+                    .when(should_show_cycle_row, |this| {
+                        this.child(
+                            h_flex()
+                                .pt_1()
+                                .gap_2()
+                                .border_t_1()
+                                .border_color(cx.theme().colors().border_variant)
+                                .justify_between()
+                                .child(Label::new("Cycle Favorited Models"))
+                                .child(KeyBinding::for_action_in(
+                                    &CycleFavoriteModels,
+                                    &focus_handle,
+                                    cx,
+                                )),
+                        )
+                    })
+                    .into_any()
+            }
+        });
+
         PickerPopoverMenu::new(
             self.language_model_selector.clone(),
             ButtonLike::new("active-model")
@@ -2217,9 +2300,7 @@ impl TextThreadEditor {
                         )
                         .child(Icon::new(icon).color(color).size(IconSize::XSmall)),
                 ),
-            move |_window, cx| {
-                Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
-            },
+            tooltip,
             gpui::Corner::BottomRight,
             cx,
         )
@@ -2572,6 +2653,7 @@ impl Render for TextThreadEditor {
             .capture_action(cx.listener(TextThreadEditor::copy))
             .capture_action(cx.listener(TextThreadEditor::cut))
             .capture_action(cx.listener(TextThreadEditor::paste))
+            .on_action(cx.listener(TextThreadEditor::paste_raw))
             .capture_action(cx.listener(TextThreadEditor::cycle_message_role))
             .capture_action(cx.listener(TextThreadEditor::confirm_command))
             .on_action(cx.listener(TextThreadEditor::assist))
@@ -2579,6 +2661,11 @@ impl Render for TextThreadEditor {
             .on_action(move |_: &ToggleModelSelector, window, cx| {
                 language_model_selector.toggle(window, cx);
             })
+            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
+                this.language_model_selector.update(cx, |selector, cx| {
+                    selector.delegate.cycle_favorite_models(window, cx);
+                });
+            }))
             .size_full()
             .child(
                 div()

crates/agent_ui/src/ui/acp_onboarding_modal.rs πŸ”—

@@ -222,8 +222,8 @@ impl Render for AcpOnboardingModal {
                 acp_onboarding_event!("Canceled", trigger = "Action");
                 cx.emit(DismissEvent);
             }))
-            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
-                this.focus_handle.focus(window);
+            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
+                this.focus_handle.focus(window, cx);
             }))
             .child(illustration)
             .child(

crates/agent_ui/src/ui/claude_code_onboarding_modal.rs πŸ”—

@@ -230,8 +230,8 @@ impl Render for ClaudeCodeOnboardingModal {
                 claude_code_onboarding_event!("Canceled", trigger = "Action");
                 cx.emit(DismissEvent);
             }))
-            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
-                this.focus_handle.focus(window);
+            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
+                this.focus_handle.focus(window, cx);
             }))
             .child(illustration)
             .child(

crates/agent_ui/src/ui/model_selector_components.rs πŸ”—

@@ -1,5 +1,5 @@
 use gpui::{Action, FocusHandle, prelude::*};
-use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
+use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
 
 #[derive(IntoElement)]
 pub struct ModelSelectorHeader {
@@ -42,6 +42,8 @@ pub struct ModelSelectorListItem {
     icon: Option<IconName>,
     is_selected: bool,
     is_focused: bool,
+    is_favorite: bool,
+    on_toggle_favorite: Option<Box<dyn Fn(&App) + 'static>>,
 }
 
 impl ModelSelectorListItem {
@@ -52,6 +54,8 @@ impl ModelSelectorListItem {
             icon: None,
             is_selected: false,
             is_focused: false,
+            is_favorite: false,
+            on_toggle_favorite: None,
         }
     }
 
@@ -69,6 +73,16 @@ impl ModelSelectorListItem {
         self.is_focused = is_focused;
         self
     }
+
+    pub fn is_favorite(mut self, is_favorite: bool) -> Self {
+        self.is_favorite = is_favorite;
+        self
+    }
+
+    pub fn on_toggle_favorite(mut self, handler: impl Fn(&App) + 'static) -> Self {
+        self.on_toggle_favorite = Some(Box::new(handler));
+        self
+    }
 }
 
 impl RenderOnce for ModelSelectorListItem {
@@ -79,6 +93,8 @@ impl RenderOnce for ModelSelectorListItem {
             Color::Muted
         };
 
+        let is_favorite = self.is_favorite;
+
         ListItem::new(self.index)
             .inset(true)
             .spacing(ListItemSpacing::Sparse)
@@ -97,11 +113,24 @@ impl RenderOnce for ModelSelectorListItem {
                     .child(Label::new(self.title).truncate()),
             )
             .end_slot(div().pr_2().when(self.is_selected, |this| {
-                this.child(
-                    Icon::new(IconName::Check)
-                        .color(Color::Accent)
-                        .size(IconSize::Small),
-                )
+                this.child(Icon::new(IconName::Check).color(Color::Accent))
+            }))
+            .end_hover_slot(div().pr_1p5().when_some(self.on_toggle_favorite, {
+                |this, handle_click| {
+                    let (icon, color, tooltip) = if is_favorite {
+                        (IconName::StarFilled, Color::Accent, "Unfavorite Model")
+                    } else {
+                        (IconName::Star, Color::Default, "Favorite Model")
+                    };
+                    this.child(
+                        IconButton::new(("toggle-favorite", self.index), icon)
+                            .layer(ElevationIndex::ElevatedSurface)
+                            .icon_color(color)
+                            .icon_size(IconSize::Small)
+                            .tooltip(Tooltip::text(tooltip))
+                            .on_click(move |_, _, cx| (handle_click)(cx)),
+                    )
+                }
             }))
     }
 }

crates/agent_ui/src/ui/onboarding_modal.rs πŸ”—

@@ -83,8 +83,8 @@ impl Render for AgentOnboardingModal {
                 agent_onboarding_event!("Canceled", trigger = "Action");
                 cx.emit(DismissEvent);
             }))
-            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
-                this.focus_handle.focus(window);
+            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
+                this.focus_handle.focus(window, cx);
             }))
             .child(
                 div()

crates/agent_ui_v2/Cargo.toml πŸ”—

@@ -12,6 +12,10 @@ workspace = true
 path = "src/agent_ui_v2.rs"
 doctest = false
 
+[features]
+test-support = ["agent/test-support"]
+
+
 [dependencies]
 agent.workspace = true
 agent_servers.workspace = true
@@ -38,3 +42,6 @@ time_format.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
+
+[dev-dependencies]
+agent = { workspace = true, features = ["test-support"] }

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

@@ -1052,6 +1052,71 @@ pub fn parse_prompt_too_long(message: &str) -> Option<u64> {
         .ok()
 }
 
+/// Request body for the token counting API.
+/// Similar to `Request` but without `max_tokens` since it's not needed for counting.
+#[derive(Debug, Serialize)]
+pub struct CountTokensRequest {
+    pub model: String,
+    pub messages: Vec<Message>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub system: Option<StringOrContents>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub tools: Vec<Tool>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub thinking: Option<Thinking>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tool_choice: Option<ToolChoice>,
+}
+
+/// Response from the token counting API.
+#[derive(Debug, Deserialize)]
+pub struct CountTokensResponse {
+    pub input_tokens: u64,
+}
+
+/// Count the number of tokens in a message without creating it.
+pub async fn count_tokens(
+    client: &dyn HttpClient,
+    api_url: &str,
+    api_key: &str,
+    request: CountTokensRequest,
+) -> Result<CountTokensResponse, AnthropicError> {
+    let uri = format!("{api_url}/v1/messages/count_tokens");
+
+    let request_builder = HttpRequest::builder()
+        .method(Method::POST)
+        .uri(uri)
+        .header("Anthropic-Version", "2023-06-01")
+        .header("X-Api-Key", api_key.trim())
+        .header("Content-Type", "application/json");
+
+    let serialized_request =
+        serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
+    let http_request = request_builder
+        .body(AsyncBody::from(serialized_request))
+        .map_err(AnthropicError::BuildRequestBody)?;
+
+    let mut response = client
+        .send(http_request)
+        .await
+        .map_err(AnthropicError::HttpSend)?;
+
+    let rate_limits = RateLimitInfo::from_headers(response.headers());
+
+    if response.status().is_success() {
+        let mut body = String::new();
+        response
+            .body_mut()
+            .read_to_string(&mut body)
+            .await
+            .map_err(AnthropicError::ReadResponse)?;
+
+        serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)
+    } else {
+        Err(handle_error_response(response, rate_limits).await)
+    }
+}
+
 #[test]
 fn test_match_window_exceeded() {
     let error = ApiError {

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

@@ -87,7 +87,7 @@ pub async fn stream_completion(
                 Ok(None) => None,
                 Err(err) => Some((
                     Err(BedrockError::ClientError(anyhow!(
-                        "{:?}",
+                        "{}",
                         aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
                     ))),
                     stream,

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

@@ -2155,7 +2155,7 @@ mod tests {
         let range = diff_1.inner.compare(&empty_diff.inner, &buffer).unwrap();
         assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
 
-        // Edit does not affect the diff.
+        // Edit does affects the diff because it recalculates word diffs.
         buffer.edit_via_marked_text(
             &"
                 one
@@ -2170,7 +2170,14 @@ mod tests {
             .unindent(),
         );
         let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
-        assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer));
+        assert_eq!(
+            Point::new(4, 0)..Point::new(5, 0),
+            diff_2
+                .inner
+                .compare(&diff_1.inner, &buffer)
+                .unwrap()
+                .to_point(&buffer)
+        );
 
         // Edit turns a deletion hunk into a modification.
         buffer.edit_via_marked_text(

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

@@ -1,6 +1,6 @@
 use anyhow::{Context as _, Result};
 use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
-use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
+use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
 use futures::AsyncReadExt;
 use gpui::{App, Context, Entity, Task};
 use http_client::HttpClient;
@@ -300,16 +300,6 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate {
         }));
     }
 
-    fn cycle(
-        &mut self,
-        _buffer: Entity<Buffer>,
-        _cursor_position: Anchor,
-        _direction: Direction,
-        _cx: &mut Context<Self>,
-    ) {
-        // Codestral doesn't support multiple completions, so cycling does nothing
-    }
-
     fn accept(&mut self, _cx: &mut Context<Self>) {
         log::debug!("Codestral: Completion accepted");
         self.pending_request = None;

crates/collab/src/tests/remote_editing_collaboration_tests.rs πŸ”—

@@ -859,9 +859,11 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
 
     cx_a.update(|cx| {
         release_channel::init(semver::Version::new(0, 0, 0), cx);
+        project::trusted_worktrees::init(HashMap::default(), None, None, cx);
     });
     server_cx.update(|cx| {
         release_channel::init(semver::Version::new(0, 0, 0), cx);
+        project::trusted_worktrees::init(HashMap::default(), None, None, cx);
     });
 
     let mut server = TestServer::start(cx_a.executor().clone()).await;

crates/collab_ui/src/collab_panel.rs πŸ”—

@@ -1252,7 +1252,7 @@ impl CollabPanel {
             context_menu
         });
 
-        window.focus(&context_menu.focus_handle(cx));
+        window.focus(&context_menu.focus_handle(cx), cx);
         let subscription = cx.subscribe_in(
             &context_menu,
             window,
@@ -1424,7 +1424,7 @@ impl CollabPanel {
             context_menu
         });
 
-        window.focus(&context_menu.focus_handle(cx));
+        window.focus(&context_menu.focus_handle(cx), cx);
         let subscription = cx.subscribe_in(
             &context_menu,
             window,
@@ -1487,7 +1487,7 @@ impl CollabPanel {
             })
         });
 
-        window.focus(&context_menu.focus_handle(cx));
+        window.focus(&context_menu.focus_handle(cx), cx);
         let subscription = cx.subscribe_in(
             &context_menu,
             window,
@@ -1521,9 +1521,9 @@ impl CollabPanel {
         if cx.stop_active_drag(window) {
             return;
         } else if self.take_editing_state(window, cx) {
-            window.focus(&self.filter_editor.focus_handle(cx));
+            window.focus(&self.filter_editor.focus_handle(cx), cx);
         } else if !self.reset_filter_editor_text(window, cx) {
-            self.focus_handle.focus(window);
+            self.focus_handle.focus(window, cx);
         }
 
         if self.context_menu.is_some() {
@@ -1826,7 +1826,7 @@ impl CollabPanel {
         });
         self.update_entries(false, cx);
         self.select_channel_editor();
-        window.focus(&self.channel_name_editor.focus_handle(cx));
+        window.focus(&self.channel_name_editor.focus_handle(cx), cx);
         cx.notify();
     }
 
@@ -1851,7 +1851,7 @@ impl CollabPanel {
         });
         self.update_entries(false, cx);
         self.select_channel_editor();
-        window.focus(&self.channel_name_editor.focus_handle(cx));
+        window.focus(&self.channel_name_editor.focus_handle(cx), cx);
         cx.notify();
     }
 
@@ -1900,7 +1900,7 @@ impl CollabPanel {
                 editor.set_text(channel.name.clone(), window, cx);
                 editor.select_all(&Default::default(), window, cx);
             });
-            window.focus(&self.channel_name_editor.focus_handle(cx));
+            window.focus(&self.channel_name_editor.focus_handle(cx), cx);
             self.update_entries(false, cx);
             self.select_channel_editor();
         }

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

@@ -588,7 +588,7 @@ impl PickerDelegate for CommandPaletteDelegate {
         })
         .detach_and_log_err(cx);
         let action = command.action;
-        window.focus(&self.previous_focus_handle);
+        window.focus(&self.previous_focus_handle, cx);
         self.dismissed(window, cx);
         window.dispatch_action(action, cx);
     }
@@ -784,7 +784,7 @@ mod tests {
 
         workspace.update_in(cx, |workspace, window, cx| {
             workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
-            editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
+            editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
         });
 
         cx.simulate_keystrokes("cmd-shift-p");
@@ -855,7 +855,7 @@ mod tests {
 
         workspace.update_in(cx, |workspace, window, cx| {
             workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
-            editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
+            editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
         });
 
         // Test normalize (trimming whitespace and double colons)

crates/context_server/Cargo.toml πŸ”—

@@ -29,6 +29,7 @@ schemars.workspace = true
 serde_json.workspace = true
 serde.workspace = true
 settings.workspace = true
+slotmap.workspace = true
 smol.workspace = true
 tempfile.workspace = true
 url = { workspace = true, features = ["serde"] }

crates/context_server/src/client.rs πŸ”—

@@ -6,6 +6,7 @@ use parking_lot::Mutex;
 use postage::barrier;
 use serde::{Deserialize, Serialize, de::DeserializeOwned};
 use serde_json::{Value, value::RawValue};
+use slotmap::SlotMap;
 use smol::channel;
 use std::{
     fmt,
@@ -50,7 +51,7 @@ pub(crate) struct Client {
     next_id: AtomicI32,
     outbound_tx: channel::Sender<String>,
     name: Arc<str>,
-    notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
+    subscription_set: Arc<Mutex<NotificationSubscriptionSet>>,
     response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
     #[allow(clippy::type_complexity)]
     #[allow(dead_code)]
@@ -191,21 +192,20 @@ impl Client {
         let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
         let (output_done_tx, output_done_rx) = barrier::channel();
 
-        let notification_handlers =
-            Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
+        let subscription_set = Arc::new(Mutex::new(NotificationSubscriptionSet::default()));
         let response_handlers =
             Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
         let request_handlers = Arc::new(Mutex::new(HashMap::<_, RequestHandler>::default()));
 
         let receive_input_task = cx.spawn({
-            let notification_handlers = notification_handlers.clone();
+            let subscription_set = subscription_set.clone();
             let response_handlers = response_handlers.clone();
             let request_handlers = request_handlers.clone();
             let transport = transport.clone();
             async move |cx| {
                 Self::handle_input(
                     transport,
-                    notification_handlers,
+                    subscription_set,
                     request_handlers,
                     response_handlers,
                     cx,
@@ -236,7 +236,7 @@ impl Client {
 
         Ok(Self {
             server_id,
-            notification_handlers,
+            subscription_set,
             response_handlers,
             name: server_name,
             next_id: Default::default(),
@@ -257,7 +257,7 @@ impl Client {
     /// to pending requests) and notifications (which trigger registered handlers).
     async fn handle_input(
         transport: Arc<dyn Transport>,
-        notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
+        subscription_set: Arc<Mutex<NotificationSubscriptionSet>>,
         request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
         response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
         cx: &mut AsyncApp,
@@ -282,10 +282,11 @@ impl Client {
                     handler(Ok(message.to_string()));
                 }
             } else if let Ok(notification) = serde_json::from_str::<AnyNotification>(&message) {
-                let mut notification_handlers = notification_handlers.lock();
-                if let Some(handler) = notification_handlers.get_mut(notification.method.as_str()) {
-                    handler(notification.params.unwrap_or(Value::Null), cx.clone());
-                }
+                subscription_set.lock().notify(
+                    &notification.method,
+                    notification.params.unwrap_or(Value::Null),
+                    cx,
+                )
             } else {
                 log::error!("Unhandled JSON from context_server: {}", message);
             }
@@ -451,12 +452,18 @@ impl Client {
         Ok(())
     }
 
+    #[must_use]
     pub fn on_notification(
         &self,
         method: &'static str,
         f: Box<dyn 'static + Send + FnMut(Value, AsyncApp)>,
-    ) {
-        self.notification_handlers.lock().insert(method, f);
+    ) -> NotificationSubscription {
+        let mut notification_subscriptions = self.subscription_set.lock();
+
+        NotificationSubscription {
+            id: notification_subscriptions.add_handler(method, f),
+            set: self.subscription_set.clone(),
+        }
     }
 }
 
@@ -485,3 +492,73 @@ impl fmt::Debug for Client {
             .finish_non_exhaustive()
     }
 }
+
+slotmap::new_key_type! {
+    struct NotificationSubscriptionId;
+}
+
+#[derive(Default)]
+pub struct NotificationSubscriptionSet {
+    // we have very few subscriptions at the moment
+    methods: Vec<(&'static str, Vec<NotificationSubscriptionId>)>,
+    handlers: SlotMap<NotificationSubscriptionId, NotificationHandler>,
+}
+
+impl NotificationSubscriptionSet {
+    #[must_use]
+    fn add_handler(
+        &mut self,
+        method: &'static str,
+        handler: NotificationHandler,
+    ) -> NotificationSubscriptionId {
+        let id = self.handlers.insert(handler);
+        if let Some((_, handler_ids)) = self
+            .methods
+            .iter_mut()
+            .find(|(probe_method, _)| method == *probe_method)
+        {
+            debug_assert!(
+                handler_ids.len() < 20,
+                "Too many MCP handlers for {}. Consider using a different data structure.",
+                method
+            );
+
+            handler_ids.push(id);
+        } else {
+            self.methods.push((method, vec![id]));
+        };
+        id
+    }
+
+    fn notify(&mut self, method: &str, payload: Value, cx: &mut AsyncApp) {
+        let Some((_, handler_ids)) = self
+            .methods
+            .iter_mut()
+            .find(|(probe_method, _)| method == *probe_method)
+        else {
+            return;
+        };
+
+        for handler_id in handler_ids {
+            if let Some(handler) = self.handlers.get_mut(*handler_id) {
+                handler(payload.clone(), cx.clone());
+            }
+        }
+    }
+}
+
+pub struct NotificationSubscription {
+    id: NotificationSubscriptionId,
+    set: Arc<Mutex<NotificationSubscriptionSet>>,
+}
+
+impl Drop for NotificationSubscription {
+    fn drop(&mut self) {
+        let mut set = self.set.lock();
+        set.handlers.remove(self.id);
+        set.methods.retain_mut(|(_, handler_ids)| {
+            handler_ids.retain(|id| *id != self.id);
+            !handler_ids.is_empty()
+        });
+    }
+}

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

@@ -96,22 +96,6 @@ impl ContextServer {
         self.initialize(self.new_client(cx)?).await
     }
 
-    /// Starts the context server, making sure handlers are registered before initialization happens
-    pub async fn start_with_handlers(
-        &self,
-        notification_handlers: Vec<(
-            &'static str,
-            Box<dyn 'static + Send + FnMut(serde_json::Value, AsyncApp)>,
-        )>,
-        cx: &AsyncApp,
-    ) -> Result<()> {
-        let client = self.new_client(cx)?;
-        for (method, handler) in notification_handlers {
-            client.on_notification(method, handler);
-        }
-        self.initialize(client).await
-    }
-
     fn new_client(&self, cx: &AsyncApp) -> Result<Client> {
         Ok(match &self.configuration {
             ContextServerTransport::Stdio(command, working_directory) => Client::stdio(

crates/context_server/src/protocol.rs πŸ”—

@@ -12,7 +12,7 @@ use futures::channel::oneshot;
 use gpui::AsyncApp;
 use serde_json::Value;
 
-use crate::client::Client;
+use crate::client::{Client, NotificationSubscription};
 use crate::types::{self, Notification, Request};
 
 pub struct ModelContextProtocol {
@@ -119,7 +119,7 @@ impl InitializedContextServerProtocol {
         &self,
         method: &'static str,
         f: Box<dyn 'static + Send + FnMut(Value, AsyncApp)>,
-    ) {
-        self.inner.on_notification(method, f);
+    ) -> NotificationSubscription {
+        self.inner.on_notification(method, f)
     }
 }

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

@@ -4,6 +4,7 @@ pub mod copilot_responses;
 pub mod request;
 mod sign_in;
 
+use crate::request::NextEditSuggestions;
 use crate::sign_in::initiate_sign_out;
 use ::fs::Fs;
 use anyhow::{Context as _, Result, anyhow};
@@ -18,7 +19,7 @@ use http_client::HttpClient;
 use language::language_settings::CopilotSettings;
 use language::{
     Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,
-    language_settings::{EditPredictionProvider, all_language_settings, language_settings},
+    language_settings::{EditPredictionProvider, all_language_settings},
     point_from_lsp, point_to_lsp,
 };
 use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
@@ -40,7 +41,7 @@ use std::{
     sync::Arc,
 };
 use sum_tree::Dimensions;
-use util::{ResultExt, fs::remove_matching, rel_path::RelPath};
+use util::{ResultExt, fs::remove_matching};
 use workspace::Workspace;
 
 pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;
@@ -315,6 +316,15 @@ struct GlobalCopilot(Entity<Copilot>);
 
 impl Global for GlobalCopilot {}
 
+/// Copilot's NextEditSuggestion response, with coordinates converted to Anchors.
+struct CopilotEditPrediction {
+    buffer: Entity<Buffer>,
+    range: Range<Anchor>,
+    text: String,
+    command: Option<lsp::Command>,
+    snapshot: BufferSnapshot,
+}
+
 impl Copilot {
     pub fn global(cx: &App) -> Option<Entity<Self>> {
         cx.try_global::<GlobalCopilot>()
@@ -873,101 +883,19 @@ impl Copilot {
         }
     }
 
-    pub fn completions<T>(
-        &mut self,
-        buffer: &Entity<Buffer>,
-        position: T,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<Completion>>>
-    where
-        T: ToPointUtf16,
-    {
-        self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
-    }
-
-    pub fn completions_cycling<T>(
-        &mut self,
-        buffer: &Entity<Buffer>,
-        position: T,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<Completion>>>
-    where
-        T: ToPointUtf16,
-    {
-        self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
-    }
-
-    pub fn accept_completion(
-        &mut self,
-        completion: &Completion,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        let server = match self.server.as_authenticated() {
-            Ok(server) => server,
-            Err(error) => return Task::ready(Err(error)),
-        };
-        let request =
-            server
-                .lsp
-                .request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
-                    uuid: completion.uuid.clone(),
-                });
-        cx.background_spawn(async move {
-            request
-                .await
-                .into_response()
-                .context("copilot: notify accepted")?;
-            Ok(())
-        })
-    }
-
-    pub fn discard_completions(
-        &mut self,
-        completions: &[Completion],
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        let server = match self.server.as_authenticated() {
-            Ok(server) => server,
-            Err(_) => return Task::ready(Ok(())),
-        };
-        let request =
-            server
-                .lsp
-                .request::<request::NotifyRejected>(request::NotifyRejectedParams {
-                    uuids: completions
-                        .iter()
-                        .map(|completion| completion.uuid.clone())
-                        .collect(),
-                });
-        cx.background_spawn(async move {
-            request
-                .await
-                .into_response()
-                .context("copilot: notify rejected")?;
-            Ok(())
-        })
-    }
-
-    fn request_completions<R, T>(
+    pub(crate) fn completions(
         &mut self,
         buffer: &Entity<Buffer>,
-        position: T,
+        position: Anchor,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<Completion>>>
-    where
-        R: 'static
-            + lsp::request::Request<
-                Params = request::GetCompletionsParams,
-                Result = request::GetCompletionsResult,
-            >,
-        T: ToPointUtf16,
-    {
+    ) -> Task<Result<Vec<CopilotEditPrediction>>> {
         self.register_buffer(buffer, cx);
 
         let server = match self.server.as_authenticated() {
             Ok(server) => server,
             Err(error) => return Task::ready(Err(error)),
         };
+        let buffer_entity = buffer.clone();
         let lsp = server.lsp.clone();
         let registered_buffer = server
             .registered_buffers
@@ -977,46 +905,31 @@ impl Copilot {
         let buffer = buffer.read(cx);
         let uri = registered_buffer.uri.clone();
         let position = position.to_point_utf16(buffer);
-        let settings = language_settings(
-            buffer.language_at(position).map(|l| l.name()),
-            buffer.file(),
-            cx,
-        );
-        let tab_size = settings.tab_size;
-        let hard_tabs = settings.hard_tabs;
-        let relative_path = buffer
-            .file()
-            .map_or(RelPath::empty().into(), |file| file.path().clone());
 
         cx.background_spawn(async move {
             let (version, snapshot) = snapshot.await?;
             let result = lsp
-                .request::<R>(request::GetCompletionsParams {
-                    doc: request::GetCompletionsDocument {
-                        uri,
-                        tab_size: tab_size.into(),
-                        indent_size: 1,
-                        insert_spaces: !hard_tabs,
-                        relative_path: relative_path.to_proto(),
-                        position: point_to_lsp(position),
-                        version: version.try_into().unwrap(),
-                    },
+                .request::<NextEditSuggestions>(request::NextEditSuggestionsParams {
+                    text_document: lsp::VersionedTextDocumentIdentifier { uri, version },
+                    position: point_to_lsp(position),
                 })
                 .await
                 .into_response()
                 .context("copilot: get completions")?;
             let completions = result
-                .completions
+                .edits
                 .into_iter()
                 .map(|completion| {
                     let start = snapshot
                         .clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
                     let end =
                         snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
-                    Completion {
-                        uuid: completion.uuid,
+                    CopilotEditPrediction {
+                        buffer: buffer_entity.clone(),
                         range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
                         text: completion.text,
+                        command: completion.command,
+                        snapshot: snapshot.clone(),
                     }
                 })
                 .collect();
@@ -1024,6 +937,35 @@ impl Copilot {
         })
     }
 
+    pub(crate) fn accept_completion(
+        &mut self,
+        completion: &CopilotEditPrediction,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        if let Some(command) = &completion.command {
+            let request = server
+                .lsp
+                .request::<lsp::ExecuteCommand>(lsp::ExecuteCommandParams {
+                    command: command.command.clone(),
+                    arguments: command.arguments.clone().unwrap_or_default(),
+                    ..Default::default()
+                });
+            cx.background_spawn(async move {
+                request
+                    .await
+                    .into_response()
+                    .context("copilot: notify accepted")?;
+                Ok(())
+            })
+        } else {
+            Task::ready(Ok(()))
+        }
+    }
+
     pub fn status(&self) -> Status {
         match &self.server {
             CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
@@ -1246,7 +1188,10 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
         .await;
     if should_install {
         node_runtime
-            .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)])
+            .npm_install_packages(
+                paths::copilot_dir(),
+                &[(PACKAGE_NAME, &latest_version.to_string())],
+            )
             .await?;
     }
 
@@ -1257,7 +1202,11 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
 mod tests {
     use super::*;
     use gpui::TestAppContext;
-    use util::{path, paths::PathStyle, rel_path::rel_path};
+    use util::{
+        path,
+        paths::PathStyle,
+        rel_path::{RelPath, rel_path},
+    };
 
     #[gpui::test(iterations = 10)]
     async fn test_buffer_management(cx: &mut TestAppContext) {

crates/copilot/src/copilot_edit_prediction_delegate.rs πŸ”—

@@ -1,49 +1,29 @@
-use crate::{Completion, Copilot};
+use crate::{Copilot, CopilotEditPrediction};
 use anyhow::Result;
-use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
-use gpui::{App, Context, Entity, EntityId, Task};
-use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings};
-use settings::Settings;
-use std::{path::Path, time::Duration};
+use edit_prediction_types::{EditPrediction, EditPredictionDelegate, interpolate_edits};
+use gpui::{App, Context, Entity, Task};
+use language::{Anchor, Buffer, EditPreview, OffsetRangeExt};
+use std::{ops::Range, sync::Arc, time::Duration};
 
 pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
 
 pub struct CopilotEditPredictionDelegate {
-    cycled: bool,
-    buffer_id: Option<EntityId>,
-    completions: Vec<Completion>,
-    active_completion_index: usize,
-    file_extension: Option<String>,
+    completion: Option<(CopilotEditPrediction, EditPreview)>,
     pending_refresh: Option<Task<Result<()>>>,
-    pending_cycling_refresh: Option<Task<Result<()>>>,
     copilot: Entity<Copilot>,
 }
 
 impl CopilotEditPredictionDelegate {
     pub fn new(copilot: Entity<Copilot>) -> Self {
         Self {
-            cycled: false,
-            buffer_id: None,
-            completions: Vec::new(),
-            active_completion_index: 0,
-            file_extension: None,
+            completion: None,
             pending_refresh: None,
-            pending_cycling_refresh: None,
             copilot,
         }
     }
 
-    fn active_completion(&self) -> Option<&Completion> {
-        self.completions.get(self.active_completion_index)
-    }
-
-    fn push_completion(&mut self, new_completion: Completion) {
-        for completion in &self.completions {
-            if completion.text == new_completion.text && completion.range == new_completion.range {
-                return;
-            }
-        }
-        self.completions.push(new_completion);
+    fn active_completion(&self) -> Option<&(CopilotEditPrediction, EditPreview)> {
+        self.completion.as_ref()
     }
 }
 
@@ -64,12 +44,8 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
         true
     }
 
-    fn supports_jump_to_edit() -> bool {
-        false
-    }
-
     fn is_refreshing(&self, _cx: &App) -> bool {
-        self.pending_refresh.is_some() && self.completions.is_empty()
+        self.pending_refresh.is_some() && self.completion.is_none()
     }
 
     fn is_enabled(
@@ -102,160 +78,96 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
                 })?
                 .await?;
 
-            this.update(cx, |this, cx| {
-                if !completions.is_empty() {
-                    this.cycled = false;
+            if let Some(mut completion) = completions.into_iter().next()
+                && let Some(trimmed_completion) = cx
+                    .update(|cx| trim_completion(&completion, cx))
+                    .ok()
+                    .flatten()
+            {
+                let preview = buffer
+                    .update(cx, |this, cx| {
+                        this.preview_edits(Arc::from(std::slice::from_ref(&trimmed_completion)), cx)
+                    })?
+                    .await;
+                this.update(cx, |this, cx| {
                     this.pending_refresh = None;
-                    this.pending_cycling_refresh = None;
-                    this.completions.clear();
-                    this.active_completion_index = 0;
-                    this.buffer_id = Some(buffer.entity_id());
-                    this.file_extension = buffer.read(cx).file().and_then(|file| {
-                        Some(
-                            Path::new(file.file_name(cx))
-                                .extension()?
-                                .to_str()?
-                                .to_string(),
-                        )
-                    });
-
-                    for completion in completions {
-                        this.push_completion(completion);
-                    }
+                    completion.range = trimmed_completion.0;
+                    completion.text = trimmed_completion.1.to_string();
+                    this.completion = Some((completion, preview));
+
                     cx.notify();
-                }
-            })?;
+                })?;
+            }
 
             Ok(())
         }));
     }
 
-    fn cycle(
-        &mut self,
-        buffer: Entity<Buffer>,
-        cursor_position: language::Anchor,
-        direction: Direction,
-        cx: &mut Context<Self>,
-    ) {
-        if self.cycled {
-            match direction {
-                Direction::Prev => {
-                    self.active_completion_index = if self.active_completion_index == 0 {
-                        self.completions.len().saturating_sub(1)
-                    } else {
-                        self.active_completion_index - 1
-                    };
-                }
-                Direction::Next => {
-                    if self.completions.is_empty() {
-                        self.active_completion_index = 0
-                    } else {
-                        self.active_completion_index =
-                            (self.active_completion_index + 1) % self.completions.len();
-                    }
-                }
-            }
-
-            cx.notify();
-        } else {
-            let copilot = self.copilot.clone();
-            self.pending_cycling_refresh = Some(cx.spawn(async move |this, cx| {
-                let completions = copilot
-                    .update(cx, |copilot, cx| {
-                        copilot.completions_cycling(&buffer, cursor_position, cx)
-                    })?
-                    .await?;
-
-                this.update(cx, |this, cx| {
-                    this.cycled = true;
-                    this.file_extension = buffer.read(cx).file().and_then(|file| {
-                        Some(
-                            Path::new(file.file_name(cx))
-                                .extension()?
-                                .to_str()?
-                                .to_string(),
-                        )
-                    });
-                    for completion in completions {
-                        this.push_completion(completion);
-                    }
-                    this.cycle(buffer, cursor_position, direction, cx);
-                })?;
-
-                Ok(())
-            }));
-        }
-    }
-
     fn accept(&mut self, cx: &mut Context<Self>) {
-        if let Some(completion) = self.active_completion() {
+        if let Some((completion, _)) = self.active_completion() {
             self.copilot
                 .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
                 .detach_and_log_err(cx);
         }
     }
 
-    fn discard(&mut self, cx: &mut Context<Self>) {
-        let settings = AllLanguageSettings::get_global(cx);
-
-        let copilot_enabled = settings.show_edit_predictions(None, cx);
-
-        if !copilot_enabled {
-            return;
-        }
-
-        self.copilot
-            .update(cx, |copilot, cx| {
-                copilot.discard_completions(&self.completions, cx)
-            })
-            .detach_and_log_err(cx);
-    }
+    fn discard(&mut self, _: &mut Context<Self>) {}
 
     fn suggest(
         &mut self,
         buffer: &Entity<Buffer>,
-        cursor_position: language::Anchor,
+        _: language::Anchor,
         cx: &mut Context<Self>,
     ) -> Option<EditPrediction> {
         let buffer_id = buffer.entity_id();
         let buffer = buffer.read(cx);
-        let completion = self.active_completion()?;
-        if Some(buffer_id) != self.buffer_id
+        let (completion, edit_preview) = self.active_completion()?;
+
+        if Some(buffer_id) != Some(completion.buffer.entity_id())
             || !completion.range.start.is_valid(buffer)
             || !completion.range.end.is_valid(buffer)
         {
             return None;
         }
+        let edits = vec![(
+            completion.range.clone(),
+            Arc::from(completion.text.as_ref()),
+        )];
+        let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits)
+            .filter(|edits| !edits.is_empty())?;
+
+        Some(EditPrediction::Local {
+            id: None,
+            edits,
+            edit_preview: Some(edit_preview.clone()),
+        })
+    }
+}
 
-        let mut completion_range = completion.range.to_offset(buffer);
-        let prefix_len = common_prefix(
-            buffer.chars_for_range(completion_range.clone()),
-            completion.text.chars(),
-        );
-        completion_range.start += prefix_len;
-        let suffix_len = common_prefix(
-            buffer.reversed_chars_for_range(completion_range.clone()),
-            completion.text[prefix_len..].chars().rev(),
-        );
-        completion_range.end = completion_range.end.saturating_sub(suffix_len);
-
-        if completion_range.is_empty()
-            && completion_range.start == cursor_position.to_offset(buffer)
-        {
-            let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
-            if completion_text.trim().is_empty() {
-                None
-            } else {
-                let position = cursor_position.bias_right(buffer);
-                Some(EditPrediction::Local {
-                    id: None,
-                    edits: vec![(position..position, completion_text.into())],
-                    edit_preview: None,
-                })
-            }
-        } else {
-            None
-        }
+fn trim_completion(
+    completion: &CopilotEditPrediction,
+    cx: &mut App,
+) -> Option<(Range<Anchor>, Arc<str>)> {
+    let buffer = completion.buffer.read(cx);
+    let mut completion_range = completion.range.to_offset(buffer);
+    let prefix_len = common_prefix(
+        buffer.chars_for_range(completion_range.clone()),
+        completion.text.chars(),
+    );
+    completion_range.start += prefix_len;
+    let suffix_len = common_prefix(
+        buffer.reversed_chars_for_range(completion_range.clone()),
+        completion.text[prefix_len..].chars().rev(),
+    );
+    completion_range.end = completion_range.end.saturating_sub(suffix_len);
+    let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
+    if completion_text.trim().is_empty() {
+        None
+    } else {
+        let completion_range =
+            buffer.anchor_after(completion_range.start)..buffer.anchor_after(completion_range.end);
+
+        Some((completion_range, Arc::from(completion_text)))
     }
 }
 
@@ -282,6 +194,7 @@ mod tests {
         Point,
         language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode},
     };
+    use lsp::Uri;
     use project::Project;
     use serde_json::json;
     use settings::{AllLanguageSettingsContent, SettingsStore};
@@ -337,12 +250,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "one.copilot1".into(),
                 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, window, cx| {
@@ -383,12 +299,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "one.copilot1".into(),
                 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, _, cx| {
@@ -412,12 +331,15 @@ mod tests {
         // After debouncing, new Copilot completions should be requested.
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "one.copilot2".into(),
                 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, window, cx| {
@@ -479,45 +401,6 @@ mod tests {
             assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
             assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
         });
-
-        // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
-        cx.update_editor(|editor, window, cx| {
-            editor.set_text("fn foo() {\n  \n}", window, cx);
-            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
-                s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
-            });
-        });
-        handle_copilot_completion_request(
-            &copilot_lsp,
-            vec![crate::request::Completion {
-                text: "    let x = 4;".into(),
-                range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
-                ..Default::default()
-            }],
-            vec![],
-        );
-
-        cx.update_editor(|editor, window, cx| {
-            editor.next_edit_prediction(&Default::default(), window, cx)
-        });
-        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
-        cx.update_editor(|editor, window, cx| {
-            assert!(editor.has_active_edit_prediction());
-            assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
-            assert_eq!(editor.text(cx), "fn foo() {\n  \n}");
-
-            // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
-            editor.tab(&Default::default(), window, cx);
-            assert!(editor.has_active_edit_prediction());
-            assert_eq!(editor.text(cx), "fn foo() {\n    \n}");
-            assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
-
-            // Using AcceptEditPrediction again accepts the suggestion.
-            editor.accept_edit_prediction(&Default::default(), window, cx);
-            assert!(!editor.has_active_edit_prediction());
-            assert_eq!(editor.text(cx), "fn foo() {\n    let x = 4;\n}");
-            assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
-        });
     }
 
     #[gpui::test(iterations = 10)]
@@ -570,12 +453,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "one.copilot1".into(),
                 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, window, cx| {
@@ -614,12 +500,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "one.123. copilot\n 456".into(),
                 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, window, cx| {
@@ -686,15 +575,18 @@ mod tests {
 
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "two.foo()".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         cx.update_editor(|editor, window, cx| {
-            editor.next_edit_prediction(&Default::default(), window, cx)
+            editor.show_edit_prediction(&Default::default(), window, cx)
         });
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, window, cx| {
@@ -703,15 +595,22 @@ mod tests {
             assert_eq!(editor.text(cx), "one\ntw\nthree\n");
 
             editor.backspace(&Default::default(), window, cx);
+        });
+        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+        cx.run_until_parked();
+        cx.update_editor(|editor, window, cx| {
             assert!(editor.has_active_edit_prediction());
             assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
             assert_eq!(editor.text(cx), "one\nt\nthree\n");
 
             editor.backspace(&Default::default(), window, cx);
+        });
+        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+        cx.run_until_parked();
+        cx.update_editor(|editor, window, cx| {
             assert!(editor.has_active_edit_prediction());
             assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
             assert_eq!(editor.text(cx), "one\n\nthree\n");
-
             // Deleting across the original suggestion range invalidates it.
             editor.backspace(&Default::default(), window, cx);
             assert!(!editor.has_active_edit_prediction());
@@ -753,7 +652,7 @@ mod tests {
         editor
             .update(cx, |editor, window, cx| {
                 use gpui::Focusable;
-                window.focus(&editor.focus_handle(cx));
+                window.focus(&editor.focus_handle(cx), cx);
             })
             .unwrap();
         let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
@@ -765,19 +664,22 @@ mod tests {
 
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "b = 2 + a".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         _ = editor.update(cx, |editor, window, cx| {
             // Ensure copilot suggestions are shown for the first excerpt.
             editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
             });
-            editor.next_edit_prediction(&Default::default(), window, cx);
+            editor.show_edit_prediction(&Default::default(), window, cx);
         });
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         _ = editor.update(cx, |editor, _, cx| {
@@ -791,12 +693,15 @@ mod tests {
 
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "d = 4 + c".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         _ = editor.update(cx, |editor, window, cx| {
             // Move to another excerpt, ensuring the suggestion gets cleared.
@@ -873,15 +778,18 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "two.foo()".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         cx.update_editor(|editor, window, cx| {
-            editor.next_edit_prediction(&Default::default(), window, cx)
+            editor.show_edit_prediction(&Default::default(), window, cx)
         });
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, _, cx| {
@@ -903,12 +811,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "two.foo()".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, _, cx| {
@@ -930,12 +841,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "two.foo()".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, _, cx| {
@@ -1000,7 +914,7 @@ mod tests {
         editor
             .update(cx, |editor, window, cx| {
                 use gpui::Focusable;
-                window.focus(&editor.focus_handle(cx))
+                window.focus(&editor.focus_handle(cx), cx)
             })
             .unwrap();
         let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
@@ -1011,16 +925,20 @@ mod tests {
             .unwrap();
 
         let mut copilot_requests = copilot_lsp
-            .set_request_handler::<crate::request::GetCompletions, _, _>(
+            .set_request_handler::<crate::request::NextEditSuggestions, _, _>(
                 move |_params, _cx| async move {
-                    Ok(crate::request::GetCompletionsResult {
-                        completions: vec![crate::request::Completion {
+                    Ok(crate::request::NextEditSuggestionsResult {
+                        edits: vec![crate::request::NextEditSuggestion {
                             text: "next line".into(),
                             range: lsp::Range::new(
                                 lsp::Position::new(1, 0),
                                 lsp::Position::new(1, 0),
                             ),
-                            ..Default::default()
+                            command: None,
+                            text_document: lsp::VersionedTextDocumentIdentifier {
+                                uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                                version: 0,
+                            },
                         }],
                     })
                 },
@@ -1049,23 +967,14 @@ mod tests {
 
     fn handle_copilot_completion_request(
         lsp: &lsp::FakeLanguageServer,
-        completions: Vec<crate::request::Completion>,
-        completions_cycling: Vec<crate::request::Completion>,
+        completions: Vec<crate::request::NextEditSuggestion>,
     ) {
-        lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
-            let completions = completions.clone();
-            async move {
-                Ok(crate::request::GetCompletionsResult {
-                    completions: completions.clone(),
-                })
-            }
-        });
-        lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
+        lsp.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
             move |_params, _cx| {
-                let completions_cycling = completions_cycling.clone();
+                let completions = completions.clone();
                 async move {
-                    Ok(crate::request::GetCompletionsResult {
-                        completions: completions_cycling.clone(),
+                    Ok(crate::request::NextEditSuggestionsResult {
+                        edits: completions.clone(),
                     })
                 }
             },

crates/copilot/src/request.rs πŸ”—

@@ -1,3 +1,4 @@
+use lsp::VersionedTextDocumentIdentifier;
 use serde::{Deserialize, Serialize};
 
 pub enum CheckStatus {}
@@ -88,72 +89,6 @@ impl lsp::request::Request for SignOut {
     const METHOD: &'static str = "signOut";
 }
 
-pub enum GetCompletions {}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct GetCompletionsParams {
-    pub doc: GetCompletionsDocument,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct GetCompletionsDocument {
-    pub tab_size: u32,
-    pub indent_size: u32,
-    pub insert_spaces: bool,
-    pub uri: lsp::Uri,
-    pub relative_path: String,
-    pub position: lsp::Position,
-    pub version: usize,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct GetCompletionsResult {
-    pub completions: Vec<Completion>,
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Completion {
-    pub text: String,
-    pub position: lsp::Position,
-    pub uuid: String,
-    pub range: lsp::Range,
-    pub display_text: String,
-}
-
-impl lsp::request::Request for GetCompletions {
-    type Params = GetCompletionsParams;
-    type Result = GetCompletionsResult;
-    const METHOD: &'static str = "getCompletions";
-}
-
-pub enum GetCompletionsCycling {}
-
-impl lsp::request::Request for GetCompletionsCycling {
-    type Params = GetCompletionsParams;
-    type Result = GetCompletionsResult;
-    const METHOD: &'static str = "getCompletionsCycling";
-}
-
-pub enum LogMessage {}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LogMessageParams {
-    pub level: u8,
-    pub message: String,
-    pub metadata_str: String,
-    pub extra: Vec<String>,
-}
-
-impl lsp::notification::Notification for LogMessage {
-    type Params = LogMessageParams;
-    const METHOD: &'static str = "LogMessage";
-}
-
 pub enum StatusNotification {}
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -223,3 +158,36 @@ impl lsp::request::Request for NotifyRejected {
     type Result = String;
     const METHOD: &'static str = "notifyRejected";
 }
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NextEditSuggestions;
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NextEditSuggestionsParams {
+    pub(crate) text_document: VersionedTextDocumentIdentifier,
+    pub(crate) position: lsp::Position,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NextEditSuggestion {
+    pub text: String,
+    pub text_document: VersionedTextDocumentIdentifier,
+    pub range: lsp::Range,
+    pub command: Option<lsp::Command>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NextEditSuggestionsResult {
+    pub edits: Vec<NextEditSuggestion>,
+}
+
+impl lsp::request::Request for NextEditSuggestions {
+    type Params = NextEditSuggestionsParams;
+    type Result = NextEditSuggestionsResult;
+
+    const METHOD: &'static str = "textDocument/copilotInlineEdit";
+}

crates/copilot/src/sign_in.rs πŸ”—

@@ -435,8 +435,8 @@ impl Render for CopilotCodeVerification {
             .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
                 cx.emit(DismissEvent);
             }))
-            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _| {
-                window.focus(&this.focus_handle);
+            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
+                window.focus(&this.focus_handle, cx);
             }))
             .child(
                 Vector::new(VectorName::ZedXCopilot, rems(8.), rems(4.))

crates/debugger_ui/src/debugger_panel.rs πŸ”—

@@ -577,7 +577,7 @@ impl DebugPanel {
                 menu
             });
 
-            window.focus(&context_menu.focus_handle(cx));
+            window.focus(&context_menu.focus_handle(cx), cx);
             let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
                 this.context_menu.take();
                 cx.notify();
@@ -1052,7 +1052,7 @@ impl DebugPanel {
         cx: &mut Context<Self>,
     ) {
         debug_assert!(self.sessions_with_children.contains_key(&session_item));
-        session_item.focus_handle(cx).focus(window);
+        session_item.focus_handle(cx).focus(window, cx);
         session_item.update(cx, |this, cx| {
             this.running_state().update(cx, |this, cx| {
                 this.go_to_selected_stack_frame(window, cx);

crates/debugger_ui/src/new_process_modal.rs πŸ”—

@@ -574,7 +574,7 @@ impl Render for NewProcessModal {
                     NewProcessMode::Launch => NewProcessMode::Task,
                 };
 
-                this.mode_focus_handle(cx).focus(window);
+                this.mode_focus_handle(cx).focus(window, cx);
             }))
             .on_action(
                 cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
@@ -585,7 +585,7 @@ impl Render for NewProcessModal {
                         NewProcessMode::Launch => NewProcessMode::Attach,
                     };
 
-                    this.mode_focus_handle(cx).focus(window);
+                    this.mode_focus_handle(cx).focus(window, cx);
                 }),
             )
             .child(
@@ -602,7 +602,7 @@ impl Render for NewProcessModal {
                                     NewProcessMode::Task.to_string(),
                                     cx.listener(|this, _, window, cx| {
                                         this.mode = NewProcessMode::Task;
-                                        this.mode_focus_handle(cx).focus(window);
+                                        this.mode_focus_handle(cx).focus(window, cx);
                                         cx.notify();
                                     }),
                                 )
@@ -611,7 +611,7 @@ impl Render for NewProcessModal {
                                     NewProcessMode::Debug.to_string(),
                                     cx.listener(|this, _, window, cx| {
                                         this.mode = NewProcessMode::Debug;
-                                        this.mode_focus_handle(cx).focus(window);
+                                        this.mode_focus_handle(cx).focus(window, cx);
                                         cx.notify();
                                     }),
                                 )
@@ -629,7 +629,7 @@ impl Render for NewProcessModal {
                                                 cx,
                                             );
                                         }
-                                        this.mode_focus_handle(cx).focus(window);
+                                        this.mode_focus_handle(cx).focus(window, cx);
                                         cx.notify();
                                     }),
                                 )
@@ -638,7 +638,7 @@ impl Render for NewProcessModal {
                                     NewProcessMode::Launch.to_string(),
                                     cx.listener(|this, _, window, cx| {
                                         this.mode = NewProcessMode::Launch;
-                                        this.mode_focus_handle(cx).focus(window);
+                                        this.mode_focus_handle(cx).focus(window, cx);
                                         cx.notify();
                                     }),
                                 )
@@ -840,17 +840,17 @@ impl ConfigureMode {
         }
     }
 
-    fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
-        window.focus_next();
+    fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+        window.focus_next(cx);
     }
 
     fn on_tab_prev(
         &mut self,
         _: &menu::SelectPrevious,
         window: &mut Window,
-        _: &mut Context<Self>,
+        cx: &mut Context<Self>,
     ) {
-        window.focus_prev();
+        window.focus_prev(cx);
     }
 
     fn render(
@@ -923,7 +923,7 @@ impl AttachMode {
                 window,
                 cx,
             );
-            window.focus(&modal.focus_handle(cx));
+            window.focus(&modal.focus_handle(cx), cx);
 
             modal
         });

crates/debugger_ui/src/onboarding_modal.rs πŸ”—

@@ -83,8 +83,8 @@ impl Render for DebuggerOnboardingModal {
                 debugger_onboarding_event!("Canceled", trigger = "Action");
                 cx.emit(DismissEvent);
             }))
-            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
-                this.focus_handle.focus(window);
+            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
+                this.focus_handle.focus(window, cx);
             }))
             .child(
                 div()

crates/debugger_ui/src/session/running.rs πŸ”—

@@ -604,7 +604,7 @@ impl DebugTerminal {
         let focus_handle = cx.focus_handle();
         let focus_subscription = cx.on_focus(&focus_handle, window, |this, window, cx| {
             if let Some(terminal) = this.terminal.as_ref() {
-                terminal.focus_handle(cx).focus(window);
+                terminal.focus_handle(cx).focus(window, cx);
             }
         });
 

crates/debugger_ui/src/session/running/breakpoint_list.rs πŸ”—

@@ -310,7 +310,7 @@ impl BreakpointList {
 
     fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
         if self.input.focus_handle(cx).contains_focused(window, cx) {
-            self.focus_handle.focus(window);
+            self.focus_handle.focus(window, cx);
         } else if self.strip_mode.is_some() {
             self.strip_mode.take();
             cx.notify();
@@ -364,9 +364,9 @@ impl BreakpointList {
                         }
                     }
                 }
-                self.focus_handle.focus(window);
+                self.focus_handle.focus(window, cx);
             } else {
-                handle.focus(window);
+                handle.focus(window, cx);
             }
 
             return;
@@ -627,7 +627,7 @@ impl BreakpointList {
                 .on_click({
                     let focus_handle = focus_handle.clone();
                     move |_, window, cx| {
-                        focus_handle.focus(window);
+                        focus_handle.focus(window, cx);
                         window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
                     }
                 }),
@@ -654,7 +654,7 @@ impl BreakpointList {
                     )
                     .on_click({
                         move |_, window, cx| {
-                            focus_handle.focus(window);
+                            focus_handle.focus(window, cx);
                             window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
                         }
                     }),

crates/debugger_ui/src/session/running/console.rs πŸ”—

@@ -105,7 +105,7 @@ impl Console {
             cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
             cx.on_focus(&focus_handle, window, |console, window, cx| {
                 if console.is_running(cx) {
-                    console.query_bar.focus_handle(cx).focus(window);
+                    console.query_bar.focus_handle(cx).focus(window, cx);
                 }
             }),
         ];

crates/debugger_ui/src/session/running/memory_view.rs πŸ”—

@@ -403,7 +403,7 @@ impl MemoryView {
                 this.set_placeholder_text("Write to Selected Memory Range", window, cx);
             });
             self.is_writing_memory = true;
-            self.query_editor.focus_handle(cx).focus(window);
+            self.query_editor.focus_handle(cx).focus(window, cx);
         } else {
             self.query_editor.update(cx, |this, cx| {
                 this.clear(window, cx);

crates/debugger_ui/src/session/running/variable_list.rs πŸ”—

@@ -529,7 +529,7 @@ impl VariableList {
 
     fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
         self.edited_path.take();
-        self.focus_handle.focus(window);
+        self.focus_handle.focus(window, cx);
         cx.notify();
     }
 
@@ -1067,7 +1067,7 @@ impl VariableList {
             editor.select_all(&editor::actions::SelectAll, window, cx);
             editor
         });
-        editor.focus_handle(cx).focus(window);
+        editor.focus_handle(cx).focus(window, cx);
         editor
     }
 

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

@@ -103,8 +103,9 @@ impl Model {
 
     pub fn max_output_tokens(&self) -> Option<u64> {
         match self {
-            Self::Chat => Some(8_192),
-            Self::Reasoner => Some(64_000),
+            // Their API treats this max against the context window, which means we hit the limit a lot
+            // Using the default value of None in the API instead
+            Self::Chat | Self::Reasoner => None,
             Self::Custom {
                 max_output_tokens, ..
             } => *max_output_tokens,

crates/diagnostics/src/buffer_diagnostics.rs πŸ”—

@@ -175,7 +175,7 @@ impl BufferDiagnosticsEditor {
                     // `BufferDiagnosticsEditor` instance.
                     EditorEvent::Focused => {
                         if buffer_diagnostics_editor.multibuffer.read(cx).is_empty() {
-                            window.focus(&buffer_diagnostics_editor.focus_handle);
+                            window.focus(&buffer_diagnostics_editor.focus_handle, cx);
                         }
                     }
                     EditorEvent::Blurred => {
@@ -517,7 +517,7 @@ impl BufferDiagnosticsEditor {
                                 .editor
                                 .read(cx)
                                 .focus_handle(cx)
-                                .focus(window);
+                                .focus(window, cx);
                         }
                     }
                 }
@@ -617,7 +617,7 @@ impl BufferDiagnosticsEditor {
         // not empty, focus on the editor instead, which will allow the user to
         // start interacting and editing the buffer's contents.
         if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
-            self.editor.focus_handle(cx).focus(window)
+            self.editor.focus_handle(cx).focus(window, cx)
         }
     }
 

crates/diagnostics/src/diagnostic_renderer.rs πŸ”—

@@ -315,6 +315,6 @@ impl DiagnosticBlock {
         editor.change_selections(Default::default(), window, cx, |s| {
             s.select_ranges([range.start..range.start]);
         });
-        window.focus(&editor.focus_handle(cx));
+        window.focus(&editor.focus_handle(cx), cx);
     }
 }

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

@@ -243,7 +243,7 @@ impl ProjectDiagnosticsEditor {
                 match event {
                     EditorEvent::Focused => {
                         if this.multibuffer.read(cx).is_empty() {
-                            window.focus(&this.focus_handle);
+                            window.focus(&this.focus_handle, cx);
                         }
                     }
                     EditorEvent::Blurred => this.close_diagnosticless_buffers(cx, false),
@@ -434,7 +434,7 @@ impl ProjectDiagnosticsEditor {
 
     fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
-            self.editor.focus_handle(cx).focus(window)
+            self.editor.focus_handle(cx).focus(window, cx)
         }
     }
 
@@ -650,7 +650,7 @@ impl ProjectDiagnosticsEditor {
                         })
                     });
                     if this.focus_handle.is_focused(window) {
-                        this.editor.read(cx).focus_handle(cx).focus(window);
+                        this.editor.read(cx).focus_handle(cx).focus(window, cx);
                     }
                 }
 

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

@@ -6,7 +6,7 @@ use crate::{
 use anyhow::{Context as _, Result};
 use futures::AsyncReadExt as _;
 use gpui::{
-    App, AppContext as _, Entity, SharedString, Task,
+    App, AppContext as _, Entity, Global, SharedString, Task,
     http_client::{self, AsyncBody, Method},
 };
 use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
@@ -300,14 +300,19 @@ pub const MERCURY_CREDENTIALS_URL: SharedString =
     SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
 pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
 pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
-pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
+
+struct GlobalMercuryApiKey(Entity<ApiKeyState>);
+
+impl Global for GlobalMercuryApiKey {}
 
 pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
-    MERCURY_API_KEY
-        .get_or_init(|| {
-            cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
-        })
-        .clone()
+    if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
+        return global.0.clone();
+    }
+    let entity =
+        cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
+    cx.set_global(GlobalMercuryApiKey(entity.clone()));
+    entity
 }
 
 pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {

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

@@ -131,8 +131,8 @@ impl Render for ZedPredictModal {
                 onboarding_event!("Cancelled", trigger = "Action");
                 cx.emit(DismissEvent);
             }))
-            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
-                this.focus_handle.focus(window);
+            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
+                this.focus_handle.focus(window, cx);
             }))
             .child(
                 div()

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

@@ -1,7 +1,7 @@
 use anyhow::Result;
 use futures::AsyncReadExt as _;
 use gpui::{
-    App, AppContext as _, Entity, SharedString, Task,
+    App, AppContext as _, Entity, Global, SharedString, Task,
     http_client::{self, AsyncBody, Method},
 };
 use language::{Point, ToOffset as _};
@@ -272,14 +272,19 @@ pub const SWEEP_CREDENTIALS_URL: SharedString =
     SharedString::new_static("https://autocomplete.sweep.dev");
 pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
 pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
-pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
+
+struct GlobalSweepApiKey(Entity<ApiKeyState>);
+
+impl Global for GlobalSweepApiKey {}
 
 pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
-    SWEEP_API_KEY
-        .get_or_init(|| {
-            cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
-        })
-        .clone()
+    if let Some(global) = cx.try_global::<GlobalSweepApiKey>() {
+        return global.0.clone();
+    }
+    let entity =
+        cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
+    cx.set_global(GlobalSweepApiKey(entity.clone()));
+    entity
 }
 
 pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {

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

@@ -2,7 +2,7 @@ use std::{cmp, sync::Arc};
 
 use client::{Client, UserStore};
 use cloud_llm_client::EditPredictionRejectReason;
-use edit_prediction_types::{DataCollectionState, Direction, EditPredictionDelegate};
+use edit_prediction_types::{DataCollectionState, EditPredictionDelegate};
 use gpui::{App, Entity, prelude::*};
 use language::{Buffer, ToPoint as _};
 use project::Project;
@@ -139,15 +139,6 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
         });
     }
 
-    fn cycle(
-        &mut self,
-        _buffer: Entity<language::Buffer>,
-        _cursor_position: language::Anchor,
-        _direction: Direction,
-        _cx: &mut Context<Self>,
-    ) {
-    }
-
     fn accept(&mut self, cx: &mut Context<Self>) {
         self.store.update(cx, |store, cx| {
             store.accept_current_prediction(&self.project, cx);

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

@@ -114,7 +114,7 @@ pub fn init(cx: &mut App) -> EpAppState {
         tx.send(Some(options)).log_err();
     })
     .detach();
-    let node_runtime = NodeRuntime::new(client.http_client(), None, rx, None);
+    let node_runtime = NodeRuntime::new(client.http_client(), None, rx);
 
     let extension_host_proxy = ExtensionHostProxy::global(cx);
 

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

@@ -95,13 +95,6 @@ pub trait EditPredictionDelegate: 'static + Sized {
         debounce: bool,
         cx: &mut Context<Self>,
     );
-    fn cycle(
-        &mut self,
-        buffer: Entity<Buffer>,
-        cursor_position: language::Anchor,
-        direction: Direction,
-        cx: &mut Context<Self>,
-    );
     fn accept(&mut self, cx: &mut Context<Self>);
     fn discard(&mut self, cx: &mut Context<Self>);
     fn did_show(&mut self, _cx: &mut Context<Self>) {}
@@ -136,13 +129,6 @@ pub trait EditPredictionDelegateHandle {
         debounce: bool,
         cx: &mut App,
     );
-    fn cycle(
-        &self,
-        buffer: Entity<Buffer>,
-        cursor_position: language::Anchor,
-        direction: Direction,
-        cx: &mut App,
-    );
     fn did_show(&self, cx: &mut App);
     fn accept(&self, cx: &mut App);
     fn discard(&self, cx: &mut App);
@@ -215,18 +201,6 @@ where
         })
     }
 
-    fn cycle(
-        &self,
-        buffer: Entity<Buffer>,
-        cursor_position: language::Anchor,
-        direction: Direction,
-        cx: &mut App,
-    ) {
-        self.update(cx, |this, cx| {
-            this.cycle(buffer, cursor_position, direction, cx)
-        })
-    }
-
     fn accept(&self, cx: &mut App) {
         self.update(cx, |this, cx| this.accept(cx))
     }

crates/edit_prediction_ui/src/rate_prediction_modal.rs πŸ”—

@@ -305,7 +305,7 @@ impl RatePredictionsModal {
                 && prediction.id == prev_prediction.prediction.id
             {
                 if focus {
-                    window.focus(&prev_prediction.feedback_editor.focus_handle(cx));
+                    window.focus(&prev_prediction.feedback_editor.focus_handle(cx), cx);
                 }
                 return;
             }

crates/editor/benches/editor_render.rs πŸ”—

@@ -29,7 +29,7 @@ fn editor_input_with_1000_cursors(bencher: &mut Bencher<'_>, cx: &TestAppContext
             );
             editor
         });
-        window.focus(&editor.focus_handle(cx));
+        window.focus(&editor.focus_handle(cx), cx);
         editor
     });
 
@@ -72,7 +72,7 @@ fn open_editor_with_one_long_line(bencher: &mut Bencher<'_>, args: &(String, Tes
                 editor.set_style(editor::EditorStyle::default(), window, cx);
                 editor
             });
-            window.focus(&editor.focus_handle(cx));
+            window.focus(&editor.focus_handle(cx), cx);
             editor
         });
     });
@@ -100,7 +100,7 @@ fn editor_render(bencher: &mut Bencher<'_>, cx: &TestAppContext) {
             editor.set_style(editor::EditorStyle::default(), window, cx);
             editor
         });
-        window.focus(&editor.focus_handle(cx));
+        window.focus(&editor.focus_handle(cx), cx);
         editor
     });
 

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

@@ -348,6 +348,61 @@ where
         );
     }
 
+    #[gpui::test]
+    async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |language_settings| {
+            language_settings.defaults.colorize_brackets = Some(true);
+        });
+
+        let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
+        language_registry.add(markdown_lang());
+        language_registry.add(rust_lang());
+
+        let mut cx = EditorTestContext::new(cx).await;
+        cx.update_buffer(|buffer, cx| {
+            buffer.set_language_registry(language_registry.clone());
+            buffer.set_language(Some(markdown_lang()), cx);
+        });
+
+        cx.set_state(indoc! {r#"
+            fn main() {
+                let v: Vec<Stringˇ> = vec![];
+            }
+        "#});
+        cx.executor().advance_clock(Duration::from_millis(100));
+        cx.executor().run_until_parked();
+
+        assert_eq!(
+            r#"fn mainΒ«1()1Β» Β«1{
+    let v: Vec<String> = vec!Β«2[]2Β»;
+}1Β»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+"#,
+            &bracket_colors_markup(&mut cx),
+            "Markdown does not colorize <> brackets"
+        );
+
+        cx.update_buffer(|buffer, cx| {
+            buffer.set_language(Some(rust_lang()), cx);
+        });
+        cx.executor().advance_clock(Duration::from_millis(100));
+        cx.executor().run_until_parked();
+
+        assert_eq!(
+            r#"fn mainΒ«1()1Β» Β«1{
+    let v: VecΒ«2<String>2Β» = vec!Β«2[]2Β»;
+}1Β»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+"#,
+            &bracket_colors_markup(&mut cx),
+            "After switching to Rust, <> brackets are now colorized"
+        );
+    }
+
     #[gpui::test]
     async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
         init_test(cx, |language_settings| {

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

@@ -51,6 +51,8 @@ pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
 pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
 pub const COMPLETION_MENU_MIN_WIDTH: Pixels = px(280.);
 pub const COMPLETION_MENU_MAX_WIDTH: Pixels = px(540.);
+pub const CODE_ACTION_MENU_MIN_WIDTH: Pixels = px(220.);
+pub const CODE_ACTION_MENU_MAX_WIDTH: Pixels = px(540.);
 
 // Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
 // documentation not yet being parsed.
@@ -179,7 +181,7 @@ impl CodeContextMenu {
     ) -> Option<AnyElement> {
         match self {
             CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
-            CodeContextMenu::CodeActions(_) => None,
+            CodeContextMenu::CodeActions(menu) => menu.render_aside(max_size, window, cx),
         }
     }
 
@@ -1419,26 +1421,6 @@ pub enum CodeActionsItem {
 }
 
 impl CodeActionsItem {
-    fn as_task(&self) -> Option<&ResolvedTask> {
-        let Self::Task(_, task) = self else {
-            return None;
-        };
-        Some(task)
-    }
-
-    fn as_code_action(&self) -> Option<&CodeAction> {
-        let Self::CodeAction { action, .. } = self else {
-            return None;
-        };
-        Some(action)
-    }
-    fn as_debug_scenario(&self) -> Option<&DebugScenario> {
-        let Self::DebugScenario(scenario) = self else {
-            return None;
-        };
-        Some(scenario)
-    }
-
     pub fn label(&self) -> String {
         match self {
             Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
@@ -1446,6 +1428,14 @@ impl CodeActionsItem {
             Self::DebugScenario(scenario) => scenario.label.to_string(),
         }
     }
+
+    pub fn menu_label(&self) -> String {
+        match self {
+            Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
+            Self::Task(_, task) => task.resolved_label.replace("\n", ""),
+            Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
+        }
+    }
 }
 
 pub struct CodeActionsMenu {
@@ -1555,60 +1545,33 @@ impl CodeActionsMenu {
                         let item_ix = range.start + ix;
                         let selected = item_ix == selected_item;
                         let colors = cx.theme().colors();
-                        div().min_w(px(220.)).max_w(px(540.)).child(
-                            ListItem::new(item_ix)
-                                .inset(true)
-                                .toggle_state(selected)
-                                .when_some(action.as_code_action(), |this, action| {
-                                    this.child(
-                                        h_flex()
-                                            .overflow_hidden()
-                                            .when(is_quick_action_bar, |this| this.text_ui(cx))
-                                            .child(
-                                                // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
-                                                action.lsp_action.title().replace("\n", ""),
-                                            )
-                                            .when(selected, |this| {
-                                                this.text_color(colors.text_accent)
-                                            }),
-                                    )
-                                })
-                                .when_some(action.as_task(), |this, task| {
-                                    this.child(
-                                        h_flex()
-                                            .overflow_hidden()
-                                            .when(is_quick_action_bar, |this| this.text_ui(cx))
-                                            .child(task.resolved_label.replace("\n", ""))
-                                            .when(selected, |this| {
-                                                this.text_color(colors.text_accent)
-                                            }),
-                                    )
-                                })
-                                .when_some(action.as_debug_scenario(), |this, scenario| {
-                                    this.child(
-                                        h_flex()
-                                            .overflow_hidden()
-                                            .when(is_quick_action_bar, |this| this.text_ui(cx))
-                                            .child("debug: ")
-                                            .child(scenario.label.clone())
-                                            .when(selected, |this| {
-                                                this.text_color(colors.text_accent)
-                                            }),
-                                    )
-                                })
-                                .on_click(cx.listener(move |editor, _, window, cx| {
-                                    cx.stop_propagation();
-                                    if let Some(task) = editor.confirm_code_action(
-                                        &ConfirmCodeAction {
-                                            item_ix: Some(item_ix),
-                                        },
-                                        window,
-                                        cx,
-                                    ) {
-                                        task.detach_and_log_err(cx)
-                                    }
-                                })),
-                        )
+
+                        ListItem::new(item_ix)
+                            .inset(true)
+                            .toggle_state(selected)
+                            .overflow_x()
+                            .child(
+                                div()
+                                    .min_w(CODE_ACTION_MENU_MIN_WIDTH)
+                                    .max_w(CODE_ACTION_MENU_MAX_WIDTH)
+                                    .overflow_hidden()
+                                    .text_ellipsis()
+                                    .when(is_quick_action_bar, |this| this.text_ui(cx))
+                                    .when(selected, |this| this.text_color(colors.text_accent))
+                                    .child(action.menu_label()),
+                            )
+                            .on_click(cx.listener(move |editor, _, window, cx| {
+                                cx.stop_propagation();
+                                if let Some(task) = editor.confirm_code_action(
+                                    &ConfirmCodeAction {
+                                        item_ix: Some(item_ix),
+                                    },
+                                    window,
+                                    cx,
+                                ) {
+                                    task.detach_and_log_err(cx)
+                                }
+                            }))
                     })
                     .collect()
             }),
@@ -1635,4 +1598,42 @@ impl CodeActionsMenu {
 
         Popover::new().child(list).into_any_element()
     }
+
+    fn render_aside(
+        &mut self,
+        max_size: Size<Pixels>,
+        window: &mut Window,
+        _cx: &mut Context<Editor>,
+    ) -> Option<AnyElement> {
+        let Some(action) = self.actions.get(self.selected_item) else {
+            return None;
+        };
+
+        let label = action.menu_label();
+        let text_system = window.text_system();
+        let mut line_wrapper = text_system.line_wrapper(
+            window.text_style().font(),
+            window.text_style().font_size.to_pixels(window.rem_size()),
+        );
+        let is_truncated =
+            line_wrapper.should_truncate_line(&label, CODE_ACTION_MENU_MAX_WIDTH, "…");
+
+        if is_truncated.is_none() {
+            return None;
+        }
+
+        Some(
+            Popover::new()
+                .child(
+                    div()
+                        .child(label)
+                        .id("code_actions_menu_extended")
+                        .px(MENU_ASIDE_X_PADDING / 2.)
+                        .max_w(max_size.width)
+                        .max_h(max_size.height)
+                        .occlude(),
+                )
+                .into_any_element(),
+        )
+    }
 }

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

@@ -14,8 +14,57 @@
 //! - [`DisplayMap`] that adds background highlights to the regions of text.
 //!   Each one of those builds on top of preceding map.
 //!
+//! ## Structure of the display map layers
+//!
+//! Each layer in the map (and the multibuffer itself to some extent) has a few
+//! structures that are used to implement the public API available to the layer
+//! above:
+//! - a `Transform` type - this represents a region of text that the layer in
+//!   question is "managing", that it transforms into a more "processed" text
+//!   for the layer above. For example, the inlay map has an `enum Transform`
+//!   that has two variants:
+//!     - `Isomorphic`, representing a region of text that has no inlay hints (i.e.
+//!       is passed through the map transparently)
+//!     - `Inlay`, representing a location where an inlay hint is to be inserted.
+//! - a `TransformSummary` type, which is usually a struct with two fields:
+//!   [`input: TextSummary`][`TextSummary`] and [`output: TextSummary`][`TextSummary`]. Here,
+//!   `input` corresponds to "text in the layer below", and `output` corresponds to the text
+//!   exposed to the layer above. So in the inlay map case, a `Transform::Isomorphic`'s summary is
+//!   just `input = output = summary`, where `summary` is the [`TextSummary`] stored in that
+//!   variant. Conversely, a `Transform::Inlay` always has an empty `input` summary, because it's
+//!   not "replacing" any text that exists on disk. The `output` is the summary of the inlay text
+//!   to be injected. - Various newtype wrappers for co-ordinate spaces (e.g. [`WrapRow`]
+//!   represents a row index, after soft-wrapping (and all lower layers)).
+//! - A `Snapshot` type (e.g. [`InlaySnapshot`]) that captures the state of a layer at a specific
+//!   point in time.
+//! - various APIs which drill through the layers below to work with the underlying text. Notably:
+//!   - `fn text_summary_for_offset()` returns a [`TextSummary`] for the range in the co-ordinate
+//!     space that the map in question is responsible for.
+//!   - `fn <A>_point_to_<B>_point()` converts a point in co-ordinate space `A` into co-ordinate
+//!     space `B`.
+//!   - A [`RowInfo`] iterator (e.g. [`InlayBufferRows`]) and a [`Chunk`] iterator
+//!     (e.g. [`InlayChunks`])
+//!   - A `sync` function (e.g. [`InlayMap::sync`]) that takes a snapshot and list of [`Edit<T>`]s,
+//!     and returns a new snapshot and a list of transformed [`Edit<S>`]s. Note that the generic
+//!     parameter on `Edit` changes, since these methods take in edits in the co-ordinate space of
+//!     the lower layer, and return edits in their own co-ordinate space. The term "edit" is
+//!     slightly misleading, since an [`Edit<T>`] doesn't tell you what changed - rather it can be
+//!     thought of as a "region to invalidate". In theory, it would be correct to always use a
+//!     single edit that covers the entire range. However, this would lead to lots of unnecessary
+//!     recalculation.
+//!
+//! See the docs for the [`inlay_map`] module for a more in-depth explanation of how a single layer
+//! works.
+//!
 //! [Editor]: crate::Editor
 //! [EditorElement]: crate::element::EditorElement
+//! [`TextSummary`]: multi_buffer::MBTextSummary
+//! [`WrapRow`]: wrap_map::WrapRow
+//! [`InlayBufferRows`]: inlay_map::InlayBufferRows
+//! [`InlayChunks`]: inlay_map::InlayChunks
+//! [`Edit<T>`]: text::Edit
+//! [`Edit<S>`]: text::Edit
+//! [`Chunk`]: language::Chunk
 
 #[macro_use]
 mod dimensions;

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

@@ -545,7 +545,7 @@ impl BlockMap {
         {
             let max_point = wrap_snapshot.max_point();
             let edit_start = wrap_snapshot.prev_row_boundary(max_point);
-            let edit_end = max_point.row() + WrapRow(1);
+            let edit_end = max_point.row() + WrapRow(1); // this is end of file
             edits = edits.compose([WrapEdit {
                 old: edit_start..edit_end,
                 new: edit_start..edit_end,
@@ -715,6 +715,7 @@ impl BlockMap {
                         let placement = block.placement.to_wrap_row(wrap_snapshot)?;
                         if let BlockPlacement::Above(row) = placement
                             && row < new_start
+                        // this will be true more often now
                         {
                             return None;
                         }

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

@@ -1,3 +1,10 @@
+//! The inlay map. See the [`display_map`][super] docs for an overview of how the inlay map fits
+//! into the rest of the [`DisplayMap`][super::DisplayMap]. Much of the documentation for this
+//! module generalizes to other layers.
+//!
+//! The core of this module is the [`InlayMap`] struct, which maintains a vec of [`Inlay`]s, and
+//! [`InlaySnapshot`], which holds a sum tree of [`Transform`]s.
+
 use crate::{
     ChunkRenderer, HighlightStyles,
     inlays::{Inlay, InlayContent},
@@ -69,7 +76,9 @@ impl sum_tree::Item for Transform {
 
 #[derive(Clone, Debug, Default)]
 struct TransformSummary {
+    /// Summary of the text before inlays have been applied.
     input: MBTextSummary,
+    /// Summary of the text after inlays have been applied.
     output: MBTextSummary,
 }
 

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

@@ -840,35 +840,62 @@ impl WrapSnapshot {
         self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
     }
 
-    #[ztracing::instrument(skip_all, fields(point=?point, ret))]
-    pub fn prev_row_boundary(&self, mut point: WrapPoint) -> WrapRow {
+    /// Try to find a TabRow start that is also a WrapRow start
+    /// Every TabRow start is a WrapRow start
+    #[ztracing::instrument(skip_all, fields(point=?point))]
+    pub fn prev_row_boundary(&self, point: WrapPoint) -> WrapRow {
         if self.transforms.is_empty() {
             return WrapRow(0);
         }
 
-        *point.column_mut() = 0;
+        let point = WrapPoint::new(point.row(), 0);
 
         let mut cursor = self
             .transforms
             .cursor::<Dimensions<WrapPoint, TabPoint>>(());
-        // start
+
         cursor.seek(&point, Bias::Right);
-        // end
         if cursor.item().is_none() {
             cursor.prev();
         }
 
-        // start
+        //                          real newline     fake          fake
+        // text:      helloworldasldlfjasd\njdlasfalsk\naskdjfasdkfj\n
+        // dimensions v       v           v            v            v
+        // transforms |-------|-----NW----|-----W------|-----W------|
+        // cursor    ^        ^^^^^^^^^^^^^                          ^
+        //                               (^)           ^^^^^^^^^^^^^^
+        // point:                                            ^
+        // point(col_zero):                           (^)
+
         while let Some(transform) = cursor.item() {
-            if transform.is_isomorphic() && cursor.start().1.column() == 0 {
-                return cmp::min(cursor.end().0.row(), point.row());
-            } else {
-                cursor.prev();
+            if transform.is_isomorphic() {
+                // this transform only has real linefeeds
+                let tab_summary = &transform.summary.input;
+                // is the wrap just before the end of the transform a tab row?
+                // thats only if this transform has at least one newline
+                //
+                // "this wrap row is a tab row" <=> self.to_tab_point(WrapPoint::new(wrap_row, 0)).column() == 0
+
+                // Note on comparison:
+                // We have code that relies on this to be row > 1
+                // It should work with row >= 1 but it does not :(
+                //
+                // That means that if every line is wrapped we walk back all the
+                // way to the start. Which invalidates the entire state triggering
+                // a full re-render.
+                if tab_summary.lines.row > 1 {
+                    let wrap_point_at_end = cursor.end().0.row();
+                    return cmp::min(wrap_point_at_end - RowDelta(1), point.row());
+                } else if cursor.start().1.column() == 0 {
+                    return cmp::min(cursor.end().0.row(), point.row());
+                }
             }
+
+            cursor.prev();
         }
-        // end
 
-        unreachable!()
+        WrapRow(0)
     }
 
     #[ztracing::instrument(skip_all)]
@@ -891,13 +918,11 @@ impl WrapSnapshot {
     }
 
     #[cfg(test)]
-    #[ztracing::instrument(skip_all)]
     pub fn text(&self) -> String {
         self.text_chunks(WrapRow(0)).collect()
     }
 
     #[cfg(test)]
-    #[ztracing::instrument(skip_all)]
     pub fn text_chunks(&self, wrap_row: WrapRow) -> impl Iterator<Item = &str> {
         self.chunks(
             wrap_row..self.max_point().row() + WrapRow(1),
@@ -1298,6 +1323,71 @@ mod tests {
     use text::Rope;
     use theme::LoadThemes;
 
+    #[gpui::test]
+    async fn test_prev_row_boundary(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        fn test_wrap_snapshot(
+            text: &str,
+            soft_wrap_every: usize, // font size multiple
+            cx: &mut gpui::TestAppContext,
+        ) -> WrapSnapshot {
+            let text_system = cx.read(|cx| cx.text_system().clone());
+            let tab_size = 4.try_into().unwrap();
+            let font = test_font();
+            let _font_id = text_system.resolve_font(&font);
+            let font_size = px(14.0);
+            // this is very much an estimate to try and get the wrapping to
+            // occur at `soft_wrap_every` we check that it pans out for every test case
+            let soft_wrapping = Some(font_size * soft_wrap_every * 0.6);
+
+            let buffer = cx.new(|cx| language::Buffer::local(text, cx));
+            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+            let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+            let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+            let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+            let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size);
+            let tabs_snapshot = tab_map.set_max_expansion_column(32);
+            let (_wrap_map, wrap_snapshot) =
+                cx.update(|cx| WrapMap::new(tabs_snapshot, font, font_size, soft_wrapping, cx));
+
+            wrap_snapshot
+        }
+
+        // These two should pass but dont, see the comparison note in
+        // prev_row_boundary about why.
+        //
+        // //                                      0123  4567  wrap_rows
+        // let wrap_snapshot = test_wrap_snapshot("1234\n5678", 1, cx);
+        // assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8");
+        // let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
+        // assert_eq!(row.0, 3);
+
+        // //                                      012  345  678  wrap_rows
+        // let wrap_snapshot = test_wrap_snapshot("123\n456\n789", 1, cx);
+        // assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8\n9");
+        // let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
+        // assert_eq!(row.0, 5);
+
+        //                                      012345678  wrap_rows
+        let wrap_snapshot = test_wrap_snapshot("123456789", 1, cx);
+        assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8\n9");
+        let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
+        assert_eq!(row.0, 0);
+
+        //                                      111  2222    44  wrap_rows
+        let wrap_snapshot = test_wrap_snapshot("123\n4567\n\n89", 4, cx);
+        assert_eq!(wrap_snapshot.text(), "123\n4567\n\n89");
+        let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
+        assert_eq!(row.0, 2);
+
+        //                                      11  2223   wrap_rows
+        let wrap_snapshot = test_wrap_snapshot("12\n3456\n\n", 3, cx);
+        assert_eq!(wrap_snapshot.text(), "12\n345\n6\n\n");
+        let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
+        assert_eq!(row.0, 3);
+    }
+
     #[gpui::test(iterations = 100)]
     async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
         // todo this test is flaky

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

@@ -485,15 +485,6 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate {
     ) {
     }
 
-    fn cycle(
-        &mut self,
-        _buffer: gpui::Entity<language::Buffer>,
-        _cursor_position: language::Anchor,
-        _direction: edit_prediction_types::Direction,
-        _cx: &mut gpui::Context<Self>,
-    ) {
-    }
-
     fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
 
     fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
@@ -561,15 +552,6 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate {
     ) {
     }
 
-    fn cycle(
-        &mut self,
-        _buffer: gpui::Entity<language::Buffer>,
-        _cursor_position: language::Anchor,
-        _direction: edit_prediction_types::Direction,
-        _cx: &mut gpui::Context<Self>,
-    ) {
-    }
-
     fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
 
     fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}

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

@@ -73,11 +73,7 @@ pub use multi_buffer::{
 pub use split::SplittableEditor;
 pub use text::Bias;
 
-use ::git::{
-    Restore,
-    blame::{BlameEntry, ParsedCommitMessage},
-    status::FileStatus,
-};
+use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
 use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError};
 use anyhow::{Context as _, Result, anyhow, bail};
 use blink_manager::BlinkManager;
@@ -124,8 +120,9 @@ use language::{
     AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
     BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
     DiagnosticEntryRef, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
-    IndentSize, Language, LanguageName, LanguageRegistry, OffsetRangeExt, OutlineItem, Point,
-    Runnable, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
+    IndentSize, Language, LanguageName, LanguageRegistry, LanguageScope, OffsetRangeExt,
+    OutlineItem, Point, Runnable, Selection, SelectionGoal, TextObject, TransactionId,
+    TreeSitterOptions, WordsQuery,
     language_settings::{
         self, LanguageSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
         all_language_settings, language_settings,
@@ -2063,46 +2060,34 @@ impl Editor {
                                         })
                                     });
                             });
-                            let edited_buffers_already_open = {
-                                let other_editors: Vec<Entity<Editor>> = workspace
-                                    .read(cx)
-                                    .panes()
-                                    .iter()
-                                    .flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
-                                    .filter(|editor| editor.entity_id() != cx.entity_id())
-                                    .collect();
-
-                                transaction.0.keys().all(|buffer| {
-                                    other_editors.iter().any(|editor| {
-                                        let multi_buffer = editor.read(cx).buffer();
-                                        multi_buffer.read(cx).is_singleton()
-                                            && multi_buffer.read(cx).as_singleton().map_or(
-                                                false,
-                                                |singleton| {
-                                                    singleton.entity_id() == buffer.entity_id()
-                                                },
-                                            )
-                                    })
-                                })
-                            };
-                            if !edited_buffers_already_open {
-                                let workspace = workspace.downgrade();
-                                let transaction = transaction.clone();
-                                cx.defer_in(window, move |_, window, cx| {
-                                    cx.spawn_in(window, async move |editor, cx| {
-                                        Self::open_project_transaction(
-                                            &editor,
-                                            workspace,
-                                            transaction,
-                                            "Rename".to_string(),
-                                            cx,
-                                        )
-                                        .await
-                                        .ok()
-                                    })
-                                    .detach();
-                                });
-                            }
+
+                            Self::open_transaction_for_hidden_buffers(
+                                workspace,
+                                transaction.clone(),
+                                "Rename".to_string(),
+                                window,
+                                cx,
+                            );
+                        }
+                    }
+
+                    project::Event::WorkspaceEditApplied(transaction) => {
+                        let Some(workspace) = editor.workspace() else {
+                            return;
+                        };
+                        let Some(active_editor) = workspace.read(cx).active_item_as::<Self>(cx)
+                        else {
+                            return;
+                        };
+
+                        if active_editor.entity_id() == cx.entity_id() {
+                            Self::open_transaction_for_hidden_buffers(
+                                workspace,
+                                transaction.clone(),
+                                "LSP Edit".to_string(),
+                                window,
+                                cx,
+                            );
                         }
                     }
 
@@ -3827,7 +3812,7 @@ impl Editor {
     ) {
         if !self.focus_handle.is_focused(window) {
             self.last_focused_descendant = None;
-            window.focus(&self.focus_handle);
+            window.focus(&self.focus_handle, cx);
         }
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -3932,7 +3917,7 @@ impl Editor {
     ) {
         if !self.focus_handle.is_focused(window) {
             self.last_focused_descendant = None;
-            window.focus(&self.focus_handle);
+            window.focus(&self.focus_handle, cx);
         }
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
@@ -4802,205 +4787,51 @@ impl Editor {
                         let end = selection.end;
                         let selection_is_empty = start == end;
                         let language_scope = buffer.language_scope_at(start);
-                        let (
-                            comment_delimiter,
-                            doc_delimiter,
-                            insert_extra_newline,
-                            indent_on_newline,
-                            indent_on_extra_newline,
-                        ) = if let Some(language) = &language_scope {
-                            let mut insert_extra_newline =
-                                insert_extra_newline_brackets(&buffer, start..end, language)
-                                    || insert_extra_newline_tree_sitter(&buffer, start..end);
-
-                            // Comment extension on newline is allowed only for cursor selections
-                            let comment_delimiter = maybe!({
-                                if !selection_is_empty {
-                                    return None;
-                                }
-
-                                if !multi_buffer.language_settings(cx).extend_comment_on_newline {
-                                    return None;
-                                }
-
-                                let delimiters = language.line_comment_prefixes();
-                                let max_len_of_delimiter =
-                                    delimiters.iter().map(|delimiter| delimiter.len()).max()?;
-                                let (snapshot, range) =
-                                    buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
-
-                                let num_of_whitespaces = snapshot
-                                    .chars_for_range(range.clone())
-                                    .take_while(|c| c.is_whitespace())
-                                    .count();
-                                let comment_candidate = snapshot
-                                    .chars_for_range(range.clone())
-                                    .skip(num_of_whitespaces)
-                                    .take(max_len_of_delimiter)
-                                    .collect::<String>();
-                                let (delimiter, trimmed_len) = delimiters
-                                    .iter()
-                                    .filter_map(|delimiter| {
-                                        let prefix = delimiter.trim_end();
-                                        if comment_candidate.starts_with(prefix) {
-                                            Some((delimiter, prefix.len()))
-                                        } else {
-                                            None
-                                        }
-                                    })
-                                    .max_by_key(|(_, len)| *len)?;
-
-                                if let Some(BlockCommentConfig {
-                                    start: block_start, ..
-                                }) = language.block_comment()
-                                {
-                                    let block_start_trimmed = block_start.trim_end();
-                                    if block_start_trimmed.starts_with(delimiter.trim_end()) {
-                                        let line_content = snapshot
-                                            .chars_for_range(range)
-                                            .skip(num_of_whitespaces)
-                                            .take(block_start_trimmed.len())
-                                            .collect::<String>();
-
-                                        if line_content.starts_with(block_start_trimmed) {
-                                            return None;
-                                        }
+                        let (comment_delimiter, doc_delimiter, newline_formatting) =
+                            if let Some(language) = &language_scope {
+                                let mut newline_formatting =
+                                    NewlineFormatting::new(&buffer, start..end, language);
+
+                                // Comment extension on newline is allowed only for cursor selections
+                                let comment_delimiter = maybe!({
+                                    if !selection_is_empty {
+                                        return None;
                                     }
-                                }
-
-                                let cursor_is_placed_after_comment_marker =
-                                    num_of_whitespaces + trimmed_len <= start_point.column as usize;
-                                if cursor_is_placed_after_comment_marker {
-                                    Some(delimiter.clone())
-                                } else {
-                                    None
-                                }
-                            });
-
-                            let mut indent_on_newline = IndentSize::spaces(0);
-                            let mut indent_on_extra_newline = IndentSize::spaces(0);
-
-                            let doc_delimiter = maybe!({
-                                if !selection_is_empty {
-                                    return None;
-                                }
-
-                                if !multi_buffer.language_settings(cx).extend_comment_on_newline {
-                                    return None;
-                                }
-
-                                let BlockCommentConfig {
-                                    start: start_tag,
-                                    end: end_tag,
-                                    prefix: delimiter,
-                                    tab_size: len,
-                                } = language.documentation_comment()?;
-                                let is_within_block_comment = buffer
-                                    .language_scope_at(start_point)
-                                    .is_some_and(|scope| scope.override_name() == Some("comment"));
-                                if !is_within_block_comment {
-                                    return None;
-                                }
-
-                                let (snapshot, range) =
-                                    buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
-
-                                let num_of_whitespaces = snapshot
-                                    .chars_for_range(range.clone())
-                                    .take_while(|c| c.is_whitespace())
-                                    .count();
 
-                                // It is safe to use a column from MultiBufferPoint in context of a single buffer ranges, because we're only ever looking at a single line at a time.
-                                let column = start_point.column;
-                                let cursor_is_after_start_tag = {
-                                    let start_tag_len = start_tag.len();
-                                    let start_tag_line = snapshot
-                                        .chars_for_range(range.clone())
-                                        .skip(num_of_whitespaces)
-                                        .take(start_tag_len)
-                                        .collect::<String>();
-                                    if start_tag_line.starts_with(start_tag.as_ref()) {
-                                        num_of_whitespaces + start_tag_len <= column as usize
-                                    } else {
-                                        false
+                                    if !multi_buffer.language_settings(cx).extend_comment_on_newline
+                                    {
+                                        return None;
                                     }
-                                };
 
-                                let cursor_is_after_delimiter = {
-                                    let delimiter_trim = delimiter.trim_end();
-                                    let delimiter_line = snapshot
-                                        .chars_for_range(range.clone())
-                                        .skip(num_of_whitespaces)
-                                        .take(delimiter_trim.len())
-                                        .collect::<String>();
-                                    if delimiter_line.starts_with(delimiter_trim) {
-                                        num_of_whitespaces + delimiter_trim.len() <= column as usize
-                                    } else {
-                                        false
-                                    }
-                                };
+                                    return comment_delimiter_for_newline(
+                                        &start_point,
+                                        &buffer,
+                                        language,
+                                    );
+                                });
 
-                                let cursor_is_before_end_tag_if_exists = {
-                                    let mut char_position = 0u32;
-                                    let mut end_tag_offset = None;
-
-                                    'outer: for chunk in snapshot.text_for_range(range) {
-                                        if let Some(byte_pos) = chunk.find(&**end_tag) {
-                                            let chars_before_match =
-                                                chunk[..byte_pos].chars().count() as u32;
-                                            end_tag_offset =
-                                                Some(char_position + chars_before_match);
-                                            break 'outer;
-                                        }
-                                        char_position += chunk.chars().count() as u32;
+                                let doc_delimiter = maybe!({
+                                    if !selection_is_empty {
+                                        return None;
                                     }
 
-                                    if let Some(end_tag_offset) = end_tag_offset {
-                                        let cursor_is_before_end_tag = column <= end_tag_offset;
-                                        if cursor_is_after_start_tag {
-                                            if cursor_is_before_end_tag {
-                                                insert_extra_newline = true;
-                                            }
-                                            let cursor_is_at_start_of_end_tag =
-                                                column == end_tag_offset;
-                                            if cursor_is_at_start_of_end_tag {
-                                                indent_on_extra_newline.len = *len;
-                                            }
-                                        }
-                                        cursor_is_before_end_tag
-                                    } else {
-                                        true
+                                    if !multi_buffer.language_settings(cx).extend_comment_on_newline
+                                    {
+                                        return None;
                                     }
-                                };
 
-                                if (cursor_is_after_start_tag || cursor_is_after_delimiter)
-                                    && cursor_is_before_end_tag_if_exists
-                                {
-                                    if cursor_is_after_start_tag {
-                                        indent_on_newline.len = *len;
-                                    }
-                                    Some(delimiter.clone())
-                                } else {
-                                    None
-                                }
-                            });
+                                    return documentation_delimiter_for_newline(
+                                        &start_point,
+                                        &buffer,
+                                        language,
+                                        &mut newline_formatting,
+                                    );
+                                });
 
-                            (
-                                comment_delimiter,
-                                doc_delimiter,
-                                insert_extra_newline,
-                                indent_on_newline,
-                                indent_on_extra_newline,
-                            )
-                        } else {
-                            (
-                                None,
-                                None,
-                                false,
-                                IndentSize::default(),
-                                IndentSize::default(),
-                            )
-                        };
+                                (comment_delimiter, doc_delimiter, newline_formatting)
+                            } else {
+                                (None, None, NewlineFormatting::default())
+                            };
 
                         let prevent_auto_indent = doc_delimiter.is_some();
                         let delimiter = comment_delimiter.or(doc_delimiter);
@@ -5010,28 +4841,28 @@ impl Editor {
                         let mut new_text = String::with_capacity(
                             1 + capacity_for_delimiter
                                 + existing_indent.len as usize
-                                + indent_on_newline.len as usize
-                                + indent_on_extra_newline.len as usize,
+                                + newline_formatting.indent_on_newline.len as usize
+                                + newline_formatting.indent_on_extra_newline.len as usize,
                         );
                         new_text.push('\n');
                         new_text.extend(existing_indent.chars());
-                        new_text.extend(indent_on_newline.chars());
+                        new_text.extend(newline_formatting.indent_on_newline.chars());
 
                         if let Some(delimiter) = &delimiter {
                             new_text.push_str(delimiter);
                         }
 
-                        if insert_extra_newline {
+                        if newline_formatting.insert_extra_newline {
                             new_text.push('\n');
                             new_text.extend(existing_indent.chars());
-                            new_text.extend(indent_on_extra_newline.chars());
+                            new_text.extend(newline_formatting.indent_on_extra_newline.chars());
                         }
 
                         let anchor = buffer.anchor_after(end);
                         let new_selection = selection.map(|_| anchor);
                         (
                             ((start..end, new_text), prevent_auto_indent),
-                            (insert_extra_newline, new_selection),
+                            (newline_formatting.insert_extra_newline, new_selection),
                         )
                     })
                     .unzip()
@@ -6672,6 +6503,52 @@ impl Editor {
         }
     }
 
+    fn open_transaction_for_hidden_buffers(
+        workspace: Entity<Workspace>,
+        transaction: ProjectTransaction,
+        title: String,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if transaction.0.is_empty() {
+            return;
+        }
+
+        let edited_buffers_already_open = {
+            let other_editors: Vec<Entity<Editor>> = workspace
+                .read(cx)
+                .panes()
+                .iter()
+                .flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
+                .filter(|editor| editor.entity_id() != cx.entity_id())
+                .collect();
+
+            transaction.0.keys().all(|buffer| {
+                other_editors.iter().any(|editor| {
+                    let multi_buffer = editor.read(cx).buffer();
+                    multi_buffer.read(cx).is_singleton()
+                        && multi_buffer
+                            .read(cx)
+                            .as_singleton()
+                            .map_or(false, |singleton| {
+                                singleton.entity_id() == buffer.entity_id()
+                            })
+                })
+            })
+        };
+        if !edited_buffers_already_open {
+            let workspace = workspace.downgrade();
+            cx.defer_in(window, move |_, window, cx| {
+                cx.spawn_in(window, async move |editor, cx| {
+                    Self::open_project_transaction(&editor, workspace, transaction, title, cx)
+                        .await
+                        .ok()
+                })
+                .detach();
+            });
+        }
+    }
+
     pub async fn open_project_transaction(
         editor: &WeakEntity<Editor>,
         workspace: WeakEntity<Workspace>,
@@ -6831,7 +6708,7 @@ impl Editor {
                 })
             })
             .on_click(cx.listener(move |editor, _: &ClickEvent, window, cx| {
-                window.focus(&editor.focus_handle(cx));
+                window.focus(&editor.focus_handle(cx), cx);
                 editor.toggle_code_actions(
                     &crate::actions::ToggleCodeActions {
                         deployed_from: Some(crate::actions::CodeActionSource::Indicator(
@@ -7587,26 +7464,6 @@ impl Editor {
         .unwrap_or(false)
     }
 
-    fn cycle_edit_prediction(
-        &mut self,
-        direction: Direction,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<()> {
-        let provider = self.edit_prediction_provider()?;
-        let cursor = self.selections.newest_anchor().head();
-        let (buffer, cursor_buffer_position) =
-            self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
-        if self.edit_predictions_hidden_for_vim_mode || !self.should_show_edit_predictions() {
-            return None;
-        }
-
-        provider.cycle(buffer, cursor_buffer_position, direction, cx);
-        self.update_visible_edit_prediction(window, cx);
-
-        Some(())
-    }
-
     pub fn show_edit_prediction(
         &mut self,
         _: &ShowEditPrediction,
@@ -7644,42 +7501,6 @@ impl Editor {
         .detach();
     }
 
-    pub fn next_edit_prediction(
-        &mut self,
-        _: &NextEditPrediction,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if self.has_active_edit_prediction() {
-            self.cycle_edit_prediction(Direction::Next, window, cx);
-        } else {
-            let is_copilot_disabled = self
-                .refresh_edit_prediction(false, true, window, cx)
-                .is_none();
-            if is_copilot_disabled {
-                cx.propagate();
-            }
-        }
-    }
-
-    pub fn previous_edit_prediction(
-        &mut self,
-        _: &PreviousEditPrediction,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if self.has_active_edit_prediction() {
-            self.cycle_edit_prediction(Direction::Prev, window, cx);
-        } else {
-            let is_copilot_disabled = self
-                .refresh_edit_prediction(false, true, window, cx)
-                .is_none();
-            if is_copilot_disabled {
-                cx.propagate();
-            }
-        }
-    }
-
     pub fn accept_partial_edit_prediction(
         &mut self,
         granularity: EditPredictionGranularity,
@@ -8724,7 +8545,7 @@ impl Editor {
                         BreakpointEditAction::Toggle
                     };
 
-                    window.focus(&editor.focus_handle(cx));
+                    window.focus(&editor.focus_handle(cx), cx);
                     editor.edit_breakpoint_at_anchor(
                         position,
                         breakpoint.as_ref().clone(),
@@ -8916,7 +8737,7 @@ impl Editor {
                 ClickEvent::Mouse(e) => e.down.button == MouseButton::Left,
             };
 
-            window.focus(&editor.focus_handle(cx));
+            window.focus(&editor.focus_handle(cx), cx);
             editor.toggle_code_actions(
                 &ToggleCodeActions {
                     deployed_from: Some(CodeActionSource::RunMenu(row)),
@@ -11331,7 +11152,7 @@ impl Editor {
         }];
 
         let focus_handle = bp_prompt.focus_handle(cx);
-        window.focus(&focus_handle);
+        window.focus(&focus_handle, cx);
 
         let block_ids = self.insert_blocks(blocks, None, cx);
         bp_prompt.update(cx, |prompt, _| {
@@ -15534,10 +15355,9 @@ impl Editor {
         I: IntoIterator<Item = P>,
         P: AsRef<[u8]>,
     {
-        let case_sensitive = self.select_next_is_case_sensitive.map_or_else(
-            || EditorSettings::get_global(cx).search.case_sensitive,
-            |value| value,
-        );
+        let case_sensitive = self
+            .select_next_is_case_sensitive
+            .unwrap_or_else(|| EditorSettings::get_global(cx).search.case_sensitive);
 
         let mut builder = AhoCorasickBuilder::new();
         builder.ascii_case_insensitive(!case_sensitive);
@@ -18159,7 +17979,7 @@ impl Editor {
                         cx,
                     );
                     let rename_focus_handle = rename_editor.focus_handle(cx);
-                    window.focus(&rename_focus_handle);
+                    window.focus(&rename_focus_handle, cx);
                     let block_id = this.insert_blocks(
                         [BlockProperties {
                             style: BlockStyle::Flex,
@@ -18273,7 +18093,7 @@ impl Editor {
     ) -> Option<RenameState> {
         let rename = self.pending_rename.take()?;
         if rename.editor.focus_handle(cx).is_focused(window) {
-            window.focus(&self.focus_handle);
+            window.focus(&self.focus_handle, cx);
         }
 
         self.remove_blocks(
@@ -22843,7 +22663,7 @@ impl Editor {
             .take()
             .and_then(|descendant| descendant.upgrade())
         {
-            window.focus(&descendant);
+            window.focus(&descendant, cx);
         } else {
             if let Some(blame) = self.blame.as_ref() {
                 blame.update(cx, GitBlame::focus)
@@ -23474,76 +23294,256 @@ struct CompletionEdit {
     snippet: Option<Snippet>,
 }
 
-fn insert_extra_newline_brackets(
+fn comment_delimiter_for_newline(
+    start_point: &Point,
     buffer: &MultiBufferSnapshot,
-    range: Range<MultiBufferOffset>,
-    language: &language::LanguageScope,
-) -> bool {
-    let leading_whitespace_len = buffer
-        .reversed_chars_at(range.start)
-        .take_while(|c| c.is_whitespace() && *c != '\n')
-        .map(|c| c.len_utf8())
-        .sum::<usize>();
-    let trailing_whitespace_len = buffer
-        .chars_at(range.end)
-        .take_while(|c| c.is_whitespace() && *c != '\n')
-        .map(|c| c.len_utf8())
-        .sum::<usize>();
-    let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len;
-
-    language.brackets().any(|(pair, enabled)| {
-        let pair_start = pair.start.trim_end();
-        let pair_end = pair.end.trim_start();
-
-        enabled
-            && pair.newline
-            && buffer.contains_str_at(range.end, pair_end)
-            && buffer.contains_str_at(
-                range.start.saturating_sub_usize(pair_start.len()),
-                pair_start,
-            )
-    })
+    language: &LanguageScope,
+) -> Option<Arc<str>> {
+    let delimiters = language.line_comment_prefixes();
+    let max_len_of_delimiter = delimiters.iter().map(|delimiter| delimiter.len()).max()?;
+    let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
+
+    let num_of_whitespaces = snapshot
+        .chars_for_range(range.clone())
+        .take_while(|c| c.is_whitespace())
+        .count();
+    let comment_candidate = snapshot
+        .chars_for_range(range.clone())
+        .skip(num_of_whitespaces)
+        .take(max_len_of_delimiter)
+        .collect::<String>();
+    let (delimiter, trimmed_len) = delimiters
+        .iter()
+        .filter_map(|delimiter| {
+            let prefix = delimiter.trim_end();
+            if comment_candidate.starts_with(prefix) {
+                Some((delimiter, prefix.len()))
+            } else {
+                None
+            }
+        })
+        .max_by_key(|(_, len)| *len)?;
+
+    if let Some(BlockCommentConfig {
+        start: block_start, ..
+    }) = language.block_comment()
+    {
+        let block_start_trimmed = block_start.trim_end();
+        if block_start_trimmed.starts_with(delimiter.trim_end()) {
+            let line_content = snapshot
+                .chars_for_range(range)
+                .skip(num_of_whitespaces)
+                .take(block_start_trimmed.len())
+                .collect::<String>();
+
+            if line_content.starts_with(block_start_trimmed) {
+                return None;
+            }
+        }
+    }
+
+    let cursor_is_placed_after_comment_marker =
+        num_of_whitespaces + trimmed_len <= start_point.column as usize;
+    if cursor_is_placed_after_comment_marker {
+        Some(delimiter.clone())
+    } else {
+        None
+    }
 }
 
-fn insert_extra_newline_tree_sitter(
+fn documentation_delimiter_for_newline(
+    start_point: &Point,
     buffer: &MultiBufferSnapshot,
-    range: Range<MultiBufferOffset>,
-) -> bool {
-    let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() {
-        [(buffer, range, _)] => (*buffer, range.clone()),
-        _ => return false,
+    language: &LanguageScope,
+    newline_formatting: &mut NewlineFormatting,
+) -> Option<Arc<str>> {
+    let BlockCommentConfig {
+        start: start_tag,
+        end: end_tag,
+        prefix: delimiter,
+        tab_size: len,
+    } = language.documentation_comment()?;
+    let is_within_block_comment = buffer
+        .language_scope_at(*start_point)
+        .is_some_and(|scope| scope.override_name() == Some("comment"));
+    if !is_within_block_comment {
+        return None;
+    }
+
+    let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
+
+    let num_of_whitespaces = snapshot
+        .chars_for_range(range.clone())
+        .take_while(|c| c.is_whitespace())
+        .count();
+
+    // It is safe to use a column from MultiBufferPoint in context of a single buffer ranges, because we're only ever looking at a single line at a time.
+    let column = start_point.column;
+    let cursor_is_after_start_tag = {
+        let start_tag_len = start_tag.len();
+        let start_tag_line = snapshot
+            .chars_for_range(range.clone())
+            .skip(num_of_whitespaces)
+            .take(start_tag_len)
+            .collect::<String>();
+        if start_tag_line.starts_with(start_tag.as_ref()) {
+            num_of_whitespaces + start_tag_len <= column as usize
+        } else {
+            false
+        }
     };
-    let pair = {
-        let mut result: Option<BracketMatch<usize>> = None;
 
-        for pair in buffer
-            .all_bracket_ranges(range.start.0..range.end.0)
-            .filter(move |pair| {
-                pair.open_range.start <= range.start.0 && pair.close_range.end >= range.end.0
-            })
-        {
-            let len = pair.close_range.end - pair.open_range.start;
+    let cursor_is_after_delimiter = {
+        let delimiter_trim = delimiter.trim_end();
+        let delimiter_line = snapshot
+            .chars_for_range(range.clone())
+            .skip(num_of_whitespaces)
+            .take(delimiter_trim.len())
+            .collect::<String>();
+        if delimiter_line.starts_with(delimiter_trim) {
+            num_of_whitespaces + delimiter_trim.len() <= column as usize
+        } else {
+            false
+        }
+    };
 
-            if let Some(existing) = &result {
-                let existing_len = existing.close_range.end - existing.open_range.start;
-                if len > existing_len {
-                    continue;
+    let cursor_is_before_end_tag_if_exists = {
+        let mut char_position = 0u32;
+        let mut end_tag_offset = None;
+
+        'outer: for chunk in snapshot.text_for_range(range) {
+            if let Some(byte_pos) = chunk.find(&**end_tag) {
+                let chars_before_match = chunk[..byte_pos].chars().count() as u32;
+                end_tag_offset = Some(char_position + chars_before_match);
+                break 'outer;
+            }
+            char_position += chunk.chars().count() as u32;
+        }
+
+        if let Some(end_tag_offset) = end_tag_offset {
+            let cursor_is_before_end_tag = column <= end_tag_offset;
+            if cursor_is_after_start_tag {
+                if cursor_is_before_end_tag {
+                    newline_formatting.insert_extra_newline = true;
+                }
+                let cursor_is_at_start_of_end_tag = column == end_tag_offset;
+                if cursor_is_at_start_of_end_tag {
+                    newline_formatting.indent_on_extra_newline.len = *len;
                 }
             }
+            cursor_is_before_end_tag
+        } else {
+            true
+        }
+    };
 
-            result = Some(pair);
+    if (cursor_is_after_start_tag || cursor_is_after_delimiter)
+        && cursor_is_before_end_tag_if_exists
+    {
+        if cursor_is_after_start_tag {
+            newline_formatting.indent_on_newline.len = *len;
         }
+        Some(delimiter.clone())
+    } else {
+        None
+    }
+}
 
-        result
-    };
-    let Some(pair) = pair else {
-        return false;
-    };
-    pair.newline_only
-        && buffer
-            .chars_for_range(pair.open_range.end..range.start.0)
-            .chain(buffer.chars_for_range(range.end.0..pair.close_range.start))
-            .all(|c| c.is_whitespace() && c != '\n')
+#[derive(Debug, Default)]
+struct NewlineFormatting {
+    insert_extra_newline: bool,
+    indent_on_newline: IndentSize,
+    indent_on_extra_newline: IndentSize,
+}
+
+impl NewlineFormatting {
+    fn new(
+        buffer: &MultiBufferSnapshot,
+        range: Range<MultiBufferOffset>,
+        language: &LanguageScope,
+    ) -> Self {
+        Self {
+            insert_extra_newline: Self::insert_extra_newline_brackets(
+                buffer,
+                range.clone(),
+                language,
+            ) || Self::insert_extra_newline_tree_sitter(buffer, range),
+            indent_on_newline: IndentSize::spaces(0),
+            indent_on_extra_newline: IndentSize::spaces(0),
+        }
+    }
+
+    fn insert_extra_newline_brackets(
+        buffer: &MultiBufferSnapshot,
+        range: Range<MultiBufferOffset>,
+        language: &language::LanguageScope,
+    ) -> bool {
+        let leading_whitespace_len = buffer
+            .reversed_chars_at(range.start)
+            .take_while(|c| c.is_whitespace() && *c != '\n')
+            .map(|c| c.len_utf8())
+            .sum::<usize>();
+        let trailing_whitespace_len = buffer
+            .chars_at(range.end)
+            .take_while(|c| c.is_whitespace() && *c != '\n')
+            .map(|c| c.len_utf8())
+            .sum::<usize>();
+        let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len;
+
+        language.brackets().any(|(pair, enabled)| {
+            let pair_start = pair.start.trim_end();
+            let pair_end = pair.end.trim_start();
+
+            enabled
+                && pair.newline
+                && buffer.contains_str_at(range.end, pair_end)
+                && buffer.contains_str_at(
+                    range.start.saturating_sub_usize(pair_start.len()),
+                    pair_start,
+                )
+        })
+    }
+
+    fn insert_extra_newline_tree_sitter(
+        buffer: &MultiBufferSnapshot,
+        range: Range<MultiBufferOffset>,
+    ) -> bool {
+        let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() {
+            [(buffer, range, _)] => (*buffer, range.clone()),
+            _ => return false,
+        };
+        let pair = {
+            let mut result: Option<BracketMatch<usize>> = None;
+
+            for pair in buffer
+                .all_bracket_ranges(range.start.0..range.end.0)
+                .filter(move |pair| {
+                    pair.open_range.start <= range.start.0 && pair.close_range.end >= range.end.0
+                })
+            {
+                let len = pair.close_range.end - pair.open_range.start;
+
+                if let Some(existing) = &result {
+                    let existing_len = existing.close_range.end - existing.open_range.start;
+                    if len > existing_len {
+                        continue;
+                    }
+                }
+
+                result = Some(pair);
+            }
+
+            result
+        };
+        let Some(pair) = pair else {
+            return false;
+        };
+        pair.newline_only
+            && buffer
+                .chars_for_range(pair.open_range.end..range.start.0)
+                .chain(buffer.chars_for_range(range.end.0..pair.close_range.start))
+                .all(|c| c.is_whitespace() && c != '\n')
+    }
 }
 
 fn update_uncommitted_diff_for_buffer(
@@ -25909,7 +25909,7 @@ impl BreakpointPromptEditor {
         self.editor
             .update(cx, |editor, cx| {
                 editor.remove_blocks(self.block_ids.clone(), None, cx);
-                window.focus(&editor.focus_handle);
+                window.focus(&editor.focus_handle, cx);
             })
             .log_err();
     }

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

@@ -215,7 +215,8 @@ impl Settings for EditorSettings {
             },
             scrollbar: Scrollbar {
                 show: scrollbar.show.map(Into::into).unwrap(),
-                git_diff: scrollbar.git_diff.unwrap(),
+                git_diff: scrollbar.git_diff.unwrap()
+                    && content.git.unwrap().enabled.unwrap().is_git_diff_enabled(),
                 selected_text: scrollbar.selected_text.unwrap(),
                 selected_symbol: scrollbar.selected_symbol.unwrap(),
                 search_results: scrollbar.search_results.unwrap(),

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

@@ -69,7 +69,6 @@ use util::{
 use workspace::{
     CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
     OpenOptions, ViewId,
-    invalid_item_view::InvalidItemView,
     item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
     register_project_item,
 };
@@ -18201,7 +18200,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
     );
 
     editor_handle.update_in(cx, |editor, window, cx| {
-        window.focus(&editor.focus_handle(cx));
+        window.focus(&editor.focus_handle(cx), cx);
         editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
             s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
         });
@@ -20881,6 +20880,36 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
         .to_string(),
     );
 
+    cx.update_editor(|editor, window, cx| {
+        editor.move_up(&MoveUp, window, cx);
+        editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
+    });
+    cx.assert_state_with_diff(
+        indoc! { "
+        Λ‡one
+      - two
+        three
+        five
+    "}
+        .to_string(),
+    );
+
+    cx.update_editor(|editor, window, cx| {
+        editor.move_down(&MoveDown, window, cx);
+        editor.move_down(&MoveDown, window, cx);
+        editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
+    });
+    cx.assert_state_with_diff(
+        indoc! { "
+        one
+      - two
+        Λ‡three
+      - four
+        five
+    "}
+        .to_string(),
+    );
+
     cx.set_state(indoc! { "
         one
         Λ‡TWO
@@ -20920,6 +20949,66 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_toggling_adjacent_diff_hunks_2(
+    executor: BackgroundExecutor,
+    cx: &mut TestAppContext,
+) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    let diff_base = r#"
+        lineA
+        lineB
+        lineC
+        lineD
+        "#
+    .unindent();
+
+    cx.set_state(
+        &r#"
+        Λ‡lineA1
+        lineB
+        lineD
+        "#
+        .unindent(),
+    );
+    cx.set_head_text(&diff_base);
+    executor.run_until_parked();
+
+    cx.update_editor(|editor, window, cx| {
+        editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
+    });
+    executor.run_until_parked();
+    cx.assert_state_with_diff(
+        r#"
+        - lineA
+        + Λ‡lineA1
+          lineB
+          lineD
+        "#
+        .unindent(),
+    );
+
+    cx.update_editor(|editor, window, cx| {
+        editor.move_down(&MoveDown, window, cx);
+        editor.move_right(&MoveRight, window, cx);
+        editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
+    });
+    executor.run_until_parked();
+    cx.assert_state_with_diff(
+        r#"
+        - lineA
+        + lineA1
+          lˇineB
+        - lineC
+          lineD
+        "#
+        .unindent(),
+    );
+}
+
 #[gpui::test]
 async fn test_edits_around_expanded_deletion_hunks(
     executor: BackgroundExecutor,
@@ -25972,6 +26061,48 @@ async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
     "});
 }
 
+#[gpui::test]
+async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
+    let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
+    language_registry.add(markdown_lang());
+    language_registry.add(python_lang);
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.update_buffer(|buffer, cx| {
+        buffer.set_language_registry(language_registry);
+        buffer.set_language(Some(markdown_lang()), cx);
+    });
+
+    // Test that `else:` correctly outdents to match `if:` inside the Python code block
+    cx.set_state(indoc! {"
+        # Heading
+
+        ```python
+        def main():
+            if condition:
+                pass
+                Λ‡
+        ```
+    "});
+    cx.update_editor(|editor, window, cx| {
+        editor.handle_input("else:", window, cx);
+    });
+    cx.run_until_parked();
+    cx.assert_editor_state(indoc! {"
+        # Heading
+
+        ```python
+        def main():
+            if condition:
+                pass
+            else:Λ‡
+        ```
+    "});
+}
+
 #[gpui::test]
 async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
@@ -27625,11 +27756,10 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
         })
         .await
         .unwrap();
-
-    assert_eq!(
-        handle.to_any_view().entity_type(),
-        TypeId::of::<InvalidItemView>()
-    );
+    // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
+    // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
+    // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
+    assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
 }
 
 #[gpui::test]
@@ -29370,6 +29500,7 @@ async fn test_find_references_single_case(cx: &mut TestAppContext) {
 #[gpui::test]
 async fn test_local_worktree_trust(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
+    cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), None, None, cx));
 
     cx.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
@@ -29529,3 +29660,38 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) {
         trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
     assert!(can_trust_after, "worktree should be trusted after trust()");
 }
+
+#[gpui::test]
+fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
+    // This test reproduces a bug where drawing an editor at a position above the viewport
+    // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
+    // causes an infinite loop in blocks_in_range.
+    //
+    // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
+    // the content mask intersection produces visible_bounds with origin at the viewport top.
+    // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
+    // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
+    // but the while loop after seek never terminates because cursor.next() is a no-op at end.
+    init_test(cx, |_| {});
+
+    let window = cx.add_window(|_, _| gpui::Empty);
+    let mut cx = VisualTestContext::from_window(*window, cx);
+
+    let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
+    let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
+
+    // Simulate a small viewport (500x500 pixels at origin 0,0)
+    cx.simulate_resize(gpui::size(px(500.), px(500.)));
+
+    // Draw the editor at a very negative Y position, simulating an editor that's been
+    // scrolled way above the visible viewport (like in a List that has scrolled past it).
+    // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
+    // This should NOT hang - it should just render nothing.
+    cx.draw(
+        gpui::point(px(0.), px(-10000.)),
+        gpui::size(px(500.), px(3000.)),
+        |_, _| editor.clone(),
+    );
+
+    // If we get here without hanging, the test passes
+}

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

@@ -37,11 +37,7 @@ use crate::{
 use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
 use collections::{BTreeMap, HashMap};
 use file_icons::FileIcons;
-use git::{
-    Oid,
-    blame::{BlameEntry, ParsedCommitMessage},
-    status::FileStatus,
-};
+use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
 use gpui::{
     Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
     Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
@@ -594,8 +590,6 @@ impl EditorElement {
         register_action(editor, window, Editor::show_signature_help);
         register_action(editor, window, Editor::signature_help_prev);
         register_action(editor, window, Editor::signature_help_next);
-        register_action(editor, window, Editor::next_edit_prediction);
-        register_action(editor, window, Editor::previous_edit_prediction);
         register_action(editor, window, Editor::show_edit_prediction);
         register_action(editor, window, Editor::context_menu_first);
         register_action(editor, window, Editor::context_menu_prev);
@@ -9164,6 +9158,15 @@ impl Element for EditorElement {
                     let height_in_lines = f64::from(bounds.size.height / line_height);
                     let max_row = snapshot.max_point().row().as_f64();
 
+                    // Calculate how much of the editor is clipped by parent containers (e.g., List).
+                    // This allows us to only render lines that are actually visible, which is
+                    // critical for performance when large AutoHeight editors are inside Lists.
+                    let visible_bounds = window.content_mask().bounds;
+                    let clipped_top = (visible_bounds.origin.y - bounds.origin.y).max(px(0.));
+                    let clipped_top_in_lines = f64::from(clipped_top / line_height);
+                    let visible_height_in_lines =
+                        f64::from(visible_bounds.size.height / line_height);
+
                     // The max scroll position for the top of the window
                     let max_scroll_top = if matches!(
                         snapshot.mode,
@@ -9220,10 +9223,16 @@ impl Element for EditorElement {
                     let mut scroll_position = snapshot.scroll_position();
                     // The scroll position is a fractional point, the whole number of which represents
                     // the top of the window in terms of display rows.
-                    let start_row = DisplayRow(scroll_position.y as u32);
+                    // We add clipped_top_in_lines to skip rows that are clipped by parent containers,
+                    // but we don't modify scroll_position itself since the parent handles positioning.
                     let max_row = snapshot.max_point().row();
+                    let start_row = cmp::min(
+                        DisplayRow((scroll_position.y + clipped_top_in_lines).floor() as u32),
+                        max_row,
+                    );
                     let end_row = cmp::min(
-                        (scroll_position.y + height_in_lines).ceil() as u32,
+                        (scroll_position.y + clipped_top_in_lines + visible_height_in_lines).ceil()
+                            as u32,
                         max_row.next_row().0,
                     );
                     let end_row = DisplayRow(end_row);

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

@@ -3,9 +3,9 @@ use anyhow::{Context as _, Result};
 use collections::HashMap;
 
 use git::{
-    GitHostingProviderRegistry, GitRemote, Oid,
-    blame::{Blame, BlameEntry, ParsedCommitMessage},
-    parse_git_remote_url,
+    GitHostingProviderRegistry, Oid,
+    blame::{Blame, BlameEntry},
+    commit::ParsedCommitMessage,
 };
 use gpui::{
     AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task,
@@ -525,12 +525,7 @@ impl GitBlame {
                                 .git_store()
                                 .read(cx)
                                 .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
-                                .and_then(|(repo, _)| {
-                                    repo.read(cx)
-                                        .remote_upstream_url
-                                        .clone()
-                                        .or(repo.read(cx).remote_origin_url.clone())
-                                });
+                                .and_then(|(repo, _)| repo.read(cx).default_remote_url());
                             let blame_buffer = project
                                 .update(cx, |project, cx| project.blame_buffer(&buffer, None, cx));
                             Ok(async move {
@@ -554,13 +549,19 @@ impl GitBlame {
                                             entries,
                                             snapshot.max_point().row,
                                         );
-                                        let commit_details = parse_commit_messages(
-                                            messages,
-                                            remote_url,
-                                            provider_registry.clone(),
-                                        )
-                                        .await;
-
+                                        let commit_details = messages
+                                            .into_iter()
+                                            .map(|(oid, message)| {
+                                                let parsed_commit_message =
+                                                    ParsedCommitMessage::parse(
+                                                        oid.to_string(),
+                                                        message,
+                                                        remote_url.as_deref(),
+                                                        Some(provider_registry.clone()),
+                                                    );
+                                                (oid, parsed_commit_message)
+                                            })
+                                            .collect();
                                         res.push((
                                             id,
                                             snapshot,
@@ -680,55 +681,6 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
     entries
 }
 
-async fn parse_commit_messages(
-    messages: impl IntoIterator<Item = (Oid, String)>,
-    remote_url: Option<String>,
-    provider_registry: Arc<GitHostingProviderRegistry>,
-) -> HashMap<Oid, ParsedCommitMessage> {
-    let mut commit_details = HashMap::default();
-
-    let parsed_remote_url = remote_url
-        .as_deref()
-        .and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
-
-    for (oid, message) in messages {
-        let permalink = if let Some((provider, git_remote)) = parsed_remote_url.as_ref() {
-            Some(provider.build_commit_permalink(
-                git_remote,
-                git::BuildCommitPermalinkParams {
-                    sha: oid.to_string().as_str(),
-                },
-            ))
-        } else {
-            None
-        };
-
-        let remote = parsed_remote_url
-            .as_ref()
-            .map(|(provider, remote)| GitRemote {
-                host: provider.clone(),
-                owner: remote.owner.clone().into(),
-                repo: remote.repo.clone().into(),
-            });
-
-        let pull_request = parsed_remote_url
-            .as_ref()
-            .and_then(|(provider, remote)| provider.extract_pull_request(remote, &message));
-
-        commit_details.insert(
-            oid,
-            ParsedCommitMessage {
-                message: message.into(),
-                permalink,
-                remote,
-                pull_request,
-            },
-        );
-    }
-
-    commit_details
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;

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

@@ -218,7 +218,7 @@ impl Editor {
             self.hide_hovered_link(cx);
             if !hovered_link_state.links.is_empty() {
                 if !self.focus_handle.is_focused(window) {
-                    window.focus(&self.focus_handle);
+                    window.focus(&self.focus_handle, cx);
                 }
 
                 // exclude links pointing back to the current anchor

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

@@ -90,8 +90,8 @@ impl MouseContextMenu {
         // `true` when the `ContextMenu` is focused.
         let focus_handle = context_menu_focus.clone();
         cx.on_next_frame(window, move |_, window, cx| {
-            cx.on_next_frame(window, move |_, window, _cx| {
-                window.focus(&focus_handle);
+            cx.on_next_frame(window, move |_, window, cx| {
+                window.focus(&focus_handle, cx);
             });
         });
 
@@ -100,7 +100,7 @@ impl MouseContextMenu {
             move |editor, _, _event: &DismissEvent, window, cx| {
                 editor.mouse_context_menu.take();
                 if context_menu_focus.contains_focused(window, cx) {
-                    window.focus(&editor.focus_handle(cx));
+                    window.focus(&editor.focus_handle(cx), cx);
                 }
             }
         });
@@ -127,7 +127,7 @@ impl MouseContextMenu {
                 }
                 editor.mouse_context_menu.take();
                 if context_menu_focus.contains_focused(window, cx) {
-                    window.focus(&editor.focus_handle(cx));
+                    window.focus(&editor.focus_handle(cx), cx);
                 }
             },
         );
@@ -161,7 +161,7 @@ pub fn deploy_context_menu(
     cx: &mut Context<Editor>,
 ) {
     if !editor.is_focused(window) {
-        window.focus(&editor.focus_handle(cx));
+        window.focus(&editor.focus_handle(cx), cx);
     }
 
     // Don't show context menu for inline editors

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

@@ -176,11 +176,9 @@ pub fn block_content_for_tests(
 }
 
 pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> String {
-    cx.draw(
-        gpui::Point::default(),
-        size(px(3000.0), px(3000.0)),
-        |_, _| editor.clone(),
-    );
+    let draw_size = size(px(3000.0), px(3000.0));
+    cx.simulate_resize(draw_size);
+    cx.draw(gpui::Point::default(), draw_size, |_, _| editor.clone());
     let (snapshot, mut lines, blocks) = editor.update_in(cx, |editor, window, cx| {
         let snapshot = editor.snapshot(window, cx);
         let text = editor.display_text(cx);

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

@@ -126,7 +126,7 @@ impl EditorLspTestContext {
                 .read(cx)
                 .nav_history_for_item(&cx.entity());
             editor.set_nav_history(Some(nav_history));
-            window.focus(&editor.focus_handle(cx))
+            window.focus(&editor.focus_handle(cx), cx)
         });
 
         let lsp = fake_servers.next().await.unwrap();

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

@@ -78,7 +78,7 @@ impl EditorTestContext {
                 cx,
             );
 
-            window.focus(&editor.focus_handle(cx));
+            window.focus(&editor.focus_handle(cx), cx);
             editor
         });
         let editor_view = editor.root(cx).unwrap();
@@ -139,7 +139,7 @@ impl EditorTestContext {
 
         let editor = cx.add_window(|window, cx| {
             let editor = build_editor(buffer, window, cx);
-            window.focus(&editor.focus_handle(cx));
+            window.focus(&editor.focus_handle(cx), cx);
 
             editor
         });

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

@@ -422,7 +422,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
         tx.send(Some(options)).log_err();
     })
     .detach();
-    let node_runtime = NodeRuntime::new(client.http_client(), None, rx, None);
+    let node_runtime = NodeRuntime::new(client.http_client(), None, rx);
 
     let extension_host_proxy = ExtensionHostProxy::global(cx);
     debug_adapter_extension::init(extension_host_proxy.clone(), cx);

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

@@ -331,7 +331,6 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
 pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
 
 mod wit {
-
     wit_bindgen::generate!({
         skip: ["init-extension"],
         path: "./wit/since_v0.8.0",
@@ -524,6 +523,12 @@ impl wit::Guest for Component {
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
 pub struct LanguageServerId(String);
 
+impl LanguageServerId {
+    pub fn new(value: String) -> Self {
+        Self(value)
+    }
+}
+
 impl AsRef<str> for LanguageServerId {
     fn as_ref(&self) -> &str {
         &self.0
@@ -540,6 +545,12 @@ impl fmt::Display for LanguageServerId {
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
 pub struct ContextServerId(String);
 
+impl ContextServerId {
+    pub fn new(value: String) -> Self {
+        Self(value)
+    }
+}
+
 impl AsRef<str> for ContextServerId {
     fn as_ref(&self) -> &str {
         &self.0

crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs πŸ”—

@@ -736,6 +736,7 @@ impl nodejs::Host for WasmState {
             .node_runtime
             .npm_package_latest_version(&package_name)
             .await
+            .map(|v| v.to_string())
             .to_wasmtime_result()
     }
 
@@ -747,6 +748,7 @@ impl nodejs::Host for WasmState {
             .node_runtime
             .npm_package_installed_version(&self.work_dir(), &package_name)
             .await
+            .map(|option| option.map(|version| version.to_string()))
             .to_wasmtime_result()
     }
 

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

@@ -1713,7 +1713,7 @@ impl PickerDelegate for FileFinderDelegate {
                                                 ui::IconPosition::End,
                                                 Some(ToggleIncludeIgnored.boxed_clone()),
                                                 move |window, cx| {
-                                                    window.focus(&focus_handle);
+                                                    window.focus(&focus_handle, cx);
                                                     window.dispatch_action(
                                                         ToggleIncludeIgnored.boxed_clone(),
                                                         cx,

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

@@ -434,7 +434,18 @@ impl RealFs {
         for component in path.components() {
             match component {
                 std::path::Component::Prefix(_) => {
-                    let canonicalized = std::fs::canonicalize(component)?;
+                    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() {
@@ -3394,6 +3405,26 @@ mod tests {
         assert_eq!(content, "Hello");
     }
 
+    #[gpui::test]
+    #[cfg(target_os = "windows")]
+    async fn test_realfs_canonicalize(executor: BackgroundExecutor) {
+        use util::paths::SanitizedPath;
+
+        let fs = RealFs {
+            bundled_git_binary_path: None,
+            executor,
+            next_job_id: Arc::new(AtomicUsize::new(0)),
+            job_event_subscribers: Arc::new(Mutex::new(Vec::new())),
+        };
+        let temp_dir = TempDir::new().unwrap();
+        let file = temp_dir.path().join("test (1).txt");
+        let file = SanitizedPath::new(&file);
+        std::fs::write(&file, "test").unwrap();
+
+        let canonicalized = fs.canonicalize(file.as_path()).await;
+        assert!(canonicalized.is_ok());
+    }
+
     #[gpui::test]
     async fn test_rename(executor: BackgroundExecutor) {
         let fs = FakeFs::new(executor.clone());

crates/git/src/blame.rs πŸ”—

@@ -1,10 +1,9 @@
+use crate::Oid;
 use crate::commit::get_messages;
 use crate::repository::RepoPath;
-use crate::{GitRemote, Oid};
 use anyhow::{Context as _, Result};
 use collections::{HashMap, HashSet};
 use futures::AsyncWriteExt;
-use gpui::SharedString;
 use serde::{Deserialize, Serialize};
 use std::process::Stdio;
 use std::{ops::Range, path::Path};
@@ -21,14 +20,6 @@ pub struct Blame {
     pub messages: HashMap<Oid, String>,
 }
 
-#[derive(Clone, Debug, Default)]
-pub struct ParsedCommitMessage {
-    pub message: SharedString,
-    pub permalink: Option<url::Url>,
-    pub pull_request: Option<crate::hosting_provider::PullRequest>,
-    pub remote: Option<GitRemote>,
-}
-
 impl Blame {
     pub async fn for_path(
         git_binary: &Path,

crates/git/src/commit.rs πŸ”—

@@ -1,7 +1,52 @@
-use crate::{Oid, status::StatusCode};
+use crate::{
+    BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, parse_git_remote_url,
+    status::StatusCode,
+};
 use anyhow::{Context as _, Result};
 use collections::HashMap;
-use std::path::Path;
+use gpui::SharedString;
+use std::{path::Path, sync::Arc};
+
+#[derive(Clone, Debug, Default)]
+pub struct ParsedCommitMessage {
+    pub message: SharedString,
+    pub permalink: Option<url::Url>,
+    pub pull_request: Option<crate::hosting_provider::PullRequest>,
+    pub remote: Option<GitRemote>,
+}
+
+impl ParsedCommitMessage {
+    pub fn parse(
+        sha: String,
+        message: String,
+        remote_url: Option<&str>,
+        provider_registry: Option<Arc<GitHostingProviderRegistry>>,
+    ) -> Self {
+        if let Some((hosting_provider, remote)) = provider_registry
+            .and_then(|reg| remote_url.and_then(|url| parse_git_remote_url(reg, url)))
+        {
+            let pull_request = hosting_provider.extract_pull_request(&remote, &message);
+            Self {
+                message: message.into(),
+                permalink: Some(
+                    hosting_provider
+                        .build_commit_permalink(&remote, BuildCommitPermalinkParams { sha: &sha }),
+                ),
+                pull_request,
+                remote: Some(GitRemote {
+                    host: hosting_provider,
+                    owner: remote.owner.into(),
+                    repo: remote.repo.into(),
+                }),
+            }
+        } else {
+            Self {
+                message: message.into(),
+                ..Default::default()
+            }
+        }
+    }
+}
 
 pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
     if shas.is_empty() {

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

@@ -3,10 +3,7 @@ use crate::{
     commit_view::CommitView,
 };
 use editor::{BlameRenderer, Editor, hover_markdown_style};
-use git::{
-    blame::{BlameEntry, ParsedCommitMessage},
-    repository::CommitSummary,
-};
+use git::{blame::BlameEntry, commit::ParsedCommitMessage, repository::CommitSummary};
 use gpui::{
     ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle,
     TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,

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

@@ -72,32 +72,26 @@ pub fn open(
     let repository = workspace.project().read(cx).active_repository(cx);
     let style = BranchListStyle::Modal;
     workspace.toggle_modal(window, cx, |window, cx| {
-        BranchList::new(
-            Some(workspace_handle),
-            repository,
-            style,
-            rems(34.),
-            window,
-            cx,
-        )
+        BranchList::new(workspace_handle, repository, style, rems(34.), window, cx)
     })
 }
 
 pub fn popover(
+    workspace: WeakEntity<Workspace>,
     repository: Option<Entity<Repository>>,
     window: &mut Window,
     cx: &mut App,
 ) -> Entity<BranchList> {
     cx.new(|cx| {
         let list = BranchList::new(
-            None,
+            workspace,
             repository,
             BranchListStyle::Popover,
             rems(20.),
             window,
             cx,
         );
-        list.focus_handle(cx).focus(window);
+        list.focus_handle(cx).focus(window, cx);
         list
     })
 }
@@ -117,7 +111,7 @@ pub struct BranchList {
 
 impl BranchList {
     fn new(
-        workspace: Option<WeakEntity<Workspace>>,
+        workspace: WeakEntity<Workspace>,
         repository: Option<Entity<Repository>>,
         style: BranchListStyle,
         width: Rems,
@@ -316,23 +310,23 @@ impl Entry {
 
 #[derive(Clone, Copy, PartialEq)]
 enum BranchFilter {
-    /// Only show local branches
-    Local,
-    /// Only show remote branches
+    /// Show both local and remote branches.
+    All,
+    /// Only show remote branches.
     Remote,
 }
 
 impl BranchFilter {
     fn invert(&self) -> Self {
         match self {
-            BranchFilter::Local => BranchFilter::Remote,
-            BranchFilter::Remote => BranchFilter::Local,
+            BranchFilter::All => BranchFilter::Remote,
+            BranchFilter::Remote => BranchFilter::All,
         }
     }
 }
 
 pub struct BranchListDelegate {
-    workspace: Option<WeakEntity<Workspace>>,
+    workspace: WeakEntity<Workspace>,
     matches: Vec<Entry>,
     all_branches: Option<Vec<Branch>>,
     default_branch: Option<SharedString>,
@@ -360,7 +354,7 @@ enum PickerState {
 
 impl BranchListDelegate {
     fn new(
-        workspace: Option<WeakEntity<Workspace>>,
+        workspace: WeakEntity<Workspace>,
         repo: Option<Entity<Repository>>,
         style: BranchListStyle,
         cx: &mut Context<BranchList>,
@@ -375,7 +369,7 @@ impl BranchListDelegate {
             selected_index: 0,
             last_query: Default::default(),
             modifiers: Default::default(),
-            branch_filter: BranchFilter::Local,
+            branch_filter: BranchFilter::All,
             state: PickerState::List,
             focus_handle: cx.focus_handle(),
         }
@@ -464,7 +458,7 @@ impl BranchListDelegate {
                     log::error!("Failed to delete branch: {}", e);
                 }
 
-                if let Some(workspace) = workspace.and_then(|w| w.upgrade()) {
+                if let Some(workspace) = workspace.upgrade() {
                     cx.update(|_window, cx| {
                         if is_remote {
                             show_error_toast(
@@ -518,7 +512,7 @@ impl PickerDelegate for BranchListDelegate {
         match self.state {
             PickerState::List | PickerState::NewRemote | PickerState::NewBranch => {
                 match self.branch_filter {
-                    BranchFilter::Local => "Select branch…",
+                    BranchFilter::All => "Select branch or remote…",
                     BranchFilter::Remote => "Select remote…",
                 }
             }
@@ -560,8 +554,8 @@ impl PickerDelegate for BranchListDelegate {
                         self.editor_position() == PickerEditorPosition::End,
                         |this| {
                             let tooltip_label = match self.branch_filter {
-                                BranchFilter::Local => "Turn Off Remote Filter",
-                                BranchFilter::Remote => "Filter Remote Branches",
+                                BranchFilter::All => "Filter Remote Branches",
+                                BranchFilter::Remote => "Show All Branches",
                             };
 
                             this.gap_1().justify_between().child({
@@ -625,40 +619,38 @@ impl PickerDelegate for BranchListDelegate {
             return Task::ready(());
         };
 
-        let display_remotes = self.branch_filter;
+        let branch_filter = self.branch_filter;
         cx.spawn_in(window, async move |picker, cx| {
+            let branch_matches_filter = |branch: &Branch| match branch_filter {
+                BranchFilter::All => true,
+                BranchFilter::Remote => branch.is_remote(),
+            };
+
             let mut matches: Vec<Entry> = if query.is_empty() {
-                all_branches
+                let mut matches: Vec<Entry> = all_branches
                     .into_iter()
-                    .filter(|branch| {
-                        if display_remotes == BranchFilter::Remote {
-                            branch.is_remote()
-                        } else {
-                            !branch.is_remote()
-                        }
-                    })
+                    .filter(|branch| branch_matches_filter(branch))
                     .map(|branch| Entry::Branch {
                         branch,
                         positions: Vec::new(),
                     })
-                    .collect()
+                    .collect();
+
+                // Keep the existing recency sort within each group, but show local branches first.
+                matches.sort_by_key(|entry| entry.as_branch().is_some_and(|b| b.is_remote()));
+
+                matches
             } else {
                 let branches = all_branches
                     .iter()
-                    .filter(|branch| {
-                        if display_remotes == BranchFilter::Remote {
-                            branch.is_remote()
-                        } else {
-                            !branch.is_remote()
-                        }
-                    })
+                    .filter(|branch| branch_matches_filter(branch))
                     .collect::<Vec<_>>();
                 let candidates = branches
                     .iter()
                     .enumerate()
                     .map(|(ix, branch)| StringMatchCandidate::new(ix, branch.name()))
                     .collect::<Vec<StringMatchCandidate>>();
-                fuzzy::match_strings(
+                let mut matches: Vec<Entry> = fuzzy::match_strings(
                     &candidates,
                     &query,
                     true,
@@ -673,7 +665,12 @@ impl PickerDelegate for BranchListDelegate {
                     branch: branches[candidate.candidate_id].clone(),
                     positions: candidate.positions,
                 })
-                .collect()
+                .collect();
+
+                // Keep fuzzy-relevance ordering within local/remote groups, but show locals first.
+                matches.sort_by_key(|entry| entry.as_branch().is_some_and(|b| b.is_remote()));
+
+                matches
             };
             picker
                 .update(cx, |picker, _| {
@@ -841,10 +838,13 @@ impl PickerDelegate for BranchListDelegate {
             Entry::NewUrl { .. } | Entry::NewBranch { .. } | Entry::NewRemoteName { .. } => {
                 Icon::new(IconName::Plus).color(Color::Muted)
             }
-            Entry::Branch { .. } => match self.branch_filter {
-                BranchFilter::Local => Icon::new(IconName::GitBranchAlt).color(Color::Muted),
-                BranchFilter::Remote => Icon::new(IconName::Screen).color(Color::Muted),
-            },
+            Entry::Branch { branch, .. } => {
+                if branch.is_remote() {
+                    Icon::new(IconName::Screen).color(Color::Muted)
+                } else {
+                    Icon::new(IconName::GitBranchAlt).color(Color::Muted)
+                }
+            }
         };
 
         let entry_title = match entry {
@@ -874,19 +874,21 @@ impl PickerDelegate for BranchListDelegate {
             Entry::NewUrl { .. } | Entry::NewBranch { .. } | Entry::NewRemoteName { .. }
         );
 
-        let delete_branch_button = IconButton::new("delete", IconName::Trash)
-            .tooltip(move |_, cx| {
-                Tooltip::for_action_in(
-                    "Delete Branch",
-                    &branch_picker::DeleteBranch,
-                    &focus_handle,
-                    cx,
-                )
-            })
-            .on_click(cx.listener(|this, _, window, cx| {
-                let selected_idx = this.delegate.selected_index();
-                this.delegate.delete_at(selected_idx, window, cx);
-            }));
+        let deleted_branch_icon = |entry_ix: usize, is_head_branch: bool| {
+            IconButton::new(("delete", entry_ix), IconName::Trash)
+                .tooltip(move |_, cx| {
+                    Tooltip::for_action_in(
+                        "Delete Branch",
+                        &branch_picker::DeleteBranch,
+                        &focus_handle,
+                        cx,
+                    )
+                })
+                .disabled(is_head_branch)
+                .on_click(cx.listener(move |this, _, window, cx| {
+                    this.delegate.delete_at(entry_ix, window, cx);
+                }))
+        };
 
         let create_from_default_button = self.default_branch.as_ref().map(|default_branch| {
             let tooltip_label: SharedString = format!("Create New From: {default_branch}").into();
@@ -963,12 +965,12 @@ impl PickerDelegate for BranchListDelegate {
                                                             "No commits found".into(),
                                                             |subject| {
                                                                 if show_author_name
-                                                                    && author_name.is_some()
+                                                                    && let Some(author) =
+                                                                        author_name
                                                                 {
                                                                     format!(
                                                                         "{}  β€’  {}",
-                                                                        author_name.unwrap(),
-                                                                        subject
+                                                                        author, subject
                                                                     )
                                                                 } else {
                                                                     subject.to_string()
@@ -1002,10 +1004,12 @@ impl PickerDelegate for BranchListDelegate {
                     self.editor_position() == PickerEditorPosition::End && !is_new_items,
                     |this| {
                         this.map(|this| {
+                            let is_head_branch =
+                                entry.as_branch().is_some_and(|branch| branch.is_head);
                             if self.selected_index() == ix {
-                                this.end_slot(delete_branch_button)
+                                this.end_slot(deleted_branch_icon(ix, is_head_branch))
                             } else {
-                                this.end_hover_slot(delete_branch_button)
+                                this.end_hover_slot(deleted_branch_icon(ix, is_head_branch))
                             }
                         })
                     },
@@ -1036,8 +1040,8 @@ impl PickerDelegate for BranchListDelegate {
     ) -> Option<AnyElement> {
         matches!(self.state, PickerState::List).then(|| {
             let label = match self.branch_filter {
-                BranchFilter::Local => "Local",
-                BranchFilter::Remote => "Remote",
+                BranchFilter::All => "Branches",
+                BranchFilter::Remote => "Remotes",
             };
 
             ListHeader::new(label).inset(true).into_any_element()
@@ -1230,7 +1234,7 @@ mod tests {
 
     use super::*;
     use git::repository::{CommitSummary, Remote};
-    use gpui::{TestAppContext, VisualTestContext};
+    use gpui::{AppContext, TestAppContext, VisualTestContext};
     use project::{FakeFs, Project};
     use rand::{Rng, rngs::StdRng};
     use serde_json::json;
@@ -1279,35 +1283,47 @@ mod tests {
         ]
     }
 
-    fn init_branch_list_test(
+    async fn init_branch_list_test(
         repository: Option<Entity<Repository>>,
         branches: Vec<Branch>,
         cx: &mut TestAppContext,
     ) -> (Entity<BranchList>, VisualTestContext) {
-        let window = cx.add_window(|window, cx| {
-            let mut delegate =
-                BranchListDelegate::new(None, repository, BranchListStyle::Modal, cx);
-            delegate.all_branches = Some(branches);
-            let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-            let picker_focus_handle = picker.focus_handle(cx);
-            picker.update(cx, |picker, _| {
-                picker.delegate.focus_handle = picker_focus_handle.clone();
-            });
+        let fs = FakeFs::new(cx.executor());
+        let project = Project::test(fs, [], cx).await;
 
-            let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
-                cx.emit(DismissEvent);
-            });
+        let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
 
-            BranchList {
-                picker,
-                picker_focus_handle,
-                width: rems(34.),
-                _subscription,
-            }
-        });
+        let branch_list = workspace
+            .update(cx, |workspace, window, cx| {
+                cx.new(|cx| {
+                    let mut delegate = BranchListDelegate::new(
+                        workspace.weak_handle(),
+                        repository,
+                        BranchListStyle::Modal,
+                        cx,
+                    );
+                    delegate.all_branches = Some(branches);
+                    let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
+                    let picker_focus_handle = picker.focus_handle(cx);
+                    picker.update(cx, |picker, _| {
+                        picker.delegate.focus_handle = picker_focus_handle.clone();
+                    });
 
-        let branch_list = window.root(cx).unwrap();
-        let cx = VisualTestContext::from_window(*window, cx);
+                    let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
+                        cx.emit(DismissEvent);
+                    });
+
+                    BranchList {
+                        picker,
+                        picker_focus_handle,
+                        width: rems(34.),
+                        _subscription,
+                    }
+                })
+            })
+            .unwrap();
+
+        let cx = VisualTestContext::from_window(*workspace, cx);
 
         (branch_list, cx)
     }
@@ -1343,7 +1359,7 @@ mod tests {
         init_test(cx);
 
         let branches = create_test_branches();
-        let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx);
+        let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx).await;
         let cx = &mut ctx;
 
         branch_list
@@ -1419,7 +1435,7 @@ mod tests {
         .await;
         cx.run_until_parked();
 
-        let (branch_list, mut ctx) = init_branch_list_test(repository.into(), branches, cx);
+        let (branch_list, mut ctx) = init_branch_list_test(repository.into(), branches, cx).await;
         let cx = &mut ctx;
 
         update_branch_list_matches_with_empty_query(&branch_list, cx).await;
@@ -1484,7 +1500,7 @@ mod tests {
         .await;
         cx.run_until_parked();
 
-        let (branch_list, mut ctx) = init_branch_list_test(repository.into(), branches, cx);
+        let (branch_list, mut ctx) = init_branch_list_test(repository.into(), branches, cx).await;
         let cx = &mut ctx;
         // Enable remote filter
         branch_list.update(cx, |branch_list, cx| {
@@ -1532,7 +1548,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_update_remote_matches_with_query(cx: &mut TestAppContext) {
+    async fn test_branch_filter_shows_all_then_remotes_and_applies_query(cx: &mut TestAppContext) {
         init_test(cx);
 
         let branches = vec![
@@ -1542,39 +1558,54 @@ mod tests {
             create_test_branch("develop", false, None, Some(700)),
         ];
 
-        let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx);
+        let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx).await;
         let cx = &mut ctx;
 
         update_branch_list_matches_with_empty_query(&branch_list, cx).await;
 
-        // Check matches, it should match all existing branches and no option to create new branch
-        branch_list
-            .update_in(cx, |branch_list, window, cx| {
-                branch_list.picker.update(cx, |picker, cx| {
-                    assert_eq!(picker.delegate.matches.len(), 2);
-                    let branches = picker
-                        .delegate
-                        .matches
-                        .iter()
-                        .map(|be| be.name())
-                        .collect::<HashSet<_>>();
-                    assert_eq!(
-                        branches,
-                        ["feature-ui", "develop"]
-                            .into_iter()
-                            .collect::<HashSet<_>>()
-                    );
+        branch_list.update(cx, |branch_list, cx| {
+            branch_list.picker.update(cx, |picker, _cx| {
+                assert_eq!(picker.delegate.matches.len(), 4);
 
-                    // Verify the last entry is NOT the "create new branch" option
-                    let last_match = picker.delegate.matches.last().unwrap();
-                    assert!(!last_match.is_new_branch());
-                    assert!(!last_match.is_new_url());
-                    picker.delegate.branch_filter = BranchFilter::Remote;
-                    picker.delegate.update_matches(String::new(), window, cx)
-                })
+                let branches = picker
+                    .delegate
+                    .matches
+                    .iter()
+                    .map(|be| be.name())
+                    .collect::<HashSet<_>>();
+                assert_eq!(
+                    branches,
+                    ["origin/main", "fork/feature-auth", "feature-ui", "develop"]
+                        .into_iter()
+                        .collect::<HashSet<_>>()
+                );
+
+                // Locals should be listed before remotes.
+                let ordered = picker
+                    .delegate
+                    .matches
+                    .iter()
+                    .map(|be| be.name())
+                    .collect::<Vec<_>>();
+                assert_eq!(
+                    ordered,
+                    vec!["feature-ui", "develop", "origin/main", "fork/feature-auth"]
+                );
+
+                // Verify the last entry is NOT the "create new branch" option
+                let last_match = picker.delegate.matches.last().unwrap();
+                assert!(!last_match.is_new_branch());
+                assert!(!last_match.is_new_url());
             })
-            .await;
-        cx.run_until_parked();
+        });
+
+        branch_list.update(cx, |branch_list, cx| {
+            branch_list.picker.update(cx, |picker, _cx| {
+                picker.delegate.branch_filter = BranchFilter::Remote;
+            })
+        });
+
+        update_branch_list_matches_with_empty_query(&branch_list, cx).await;
 
         branch_list
             .update_in(cx, |branch_list, window, cx| {
@@ -1637,7 +1668,8 @@ mod tests {
             create_test_branch(FEATURE_BRANCH, false, None, Some(900)),
         ];
 
-        let (branch_list, mut ctx) = init_branch_list_test(repository.into(), branches, test_cx);
+        let (branch_list, mut ctx) =
+            init_branch_list_test(repository.into(), branches, test_cx).await;
         let cx = &mut ctx;
 
         branch_list
@@ -1696,7 +1728,7 @@ mod tests {
         let repository = init_fake_repository(cx).await;
         let branches = vec![create_test_branch("main", true, None, Some(1000))];
 
-        let (branch_list, mut ctx) = init_branch_list_test(repository.into(), branches, cx);
+        let (branch_list, mut ctx) = init_branch_list_test(repository.into(), branches, cx).await;
         let cx = &mut ctx;
 
         branch_list
@@ -1774,7 +1806,7 @@ mod tests {
         init_test(cx);
 
         let branches = vec![create_test_branch("main_branch", true, None, Some(1000))];
-        let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx);
+        let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx).await;
         let cx = &mut ctx;
 
         branch_list
@@ -1837,7 +1869,7 @@ mod tests {
         init_test(cx);
         let branches = vec![create_test_branch("main", true, None, Some(1000))];
 
-        let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx);
+        let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx).await;
         let cx = &mut ctx;
 
         let subscription = cx.update(|_, cx| {
@@ -1848,7 +1880,12 @@ mod tests {
 
         branch_list
             .update_in(cx, |branch_list, window, cx| {
-                window.focus(&branch_list.picker_focus_handle);
+                window.focus(&branch_list.picker_focus_handle, cx);
+                assert!(
+                    branch_list.picker_focus_handle.is_focused(window),
+                    "Branch picker should be focused when selecting an entry"
+                );
+
                 branch_list.picker.update(cx, |picker, cx| {
                     picker
                         .delegate
@@ -1860,6 +1897,9 @@ mod tests {
         cx.run_until_parked();
 
         branch_list.update_in(cx, |branch_list, window, cx| {
+            // Re-focus the picker since workspace initialization during run_until_parked
+            window.focus(&branch_list.picker_focus_handle, cx);
+
             branch_list.picker.update(cx, |picker, cx| {
                 let last_match = picker.delegate.matches.last().unwrap();
                 assert!(last_match.is_new_url());
@@ -1893,7 +1933,7 @@ mod tests {
             .map(|i| create_test_branch(&format!("branch-{:02}", i), i == 0, None, Some(i * 100)))
             .collect();
 
-        let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx);
+        let (branch_list, mut ctx) = init_branch_list_test(None, branches, cx).await;
         let cx = &mut ctx;
 
         update_branch_list_matches_with_empty_query(&branch_list, cx).await;

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

@@ -337,6 +337,7 @@ impl CommitModal {
             active_repo,
             is_amend_pending,
             is_signoff_enabled,
+            workspace,
         ) = self.git_panel.update(cx, |git_panel, cx| {
             let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
             let title = git_panel.commit_button_title();
@@ -354,6 +355,7 @@ impl CommitModal {
                 active_repo,
                 is_amend_pending,
                 is_signoff_enabled,
+                git_panel.workspace.clone(),
             )
         });
 
@@ -375,7 +377,14 @@ impl CommitModal {
             .style(ButtonStyle::Transparent);
 
         let branch_picker = PopoverMenu::new("popover-button")
-            .menu(move |window, cx| Some(branch_picker::popover(active_repo.clone(), window, cx)))
+            .menu(move |window, cx| {
+                Some(branch_picker::popover(
+                    workspace.clone(),
+                    active_repo.clone(),
+                    window,
+                    cx,
+                ))
+            })
             .with_handle(self.branch_list_handle.clone())
             .trigger_with_tooltip(
                 branch_picker_button,
@@ -512,7 +521,7 @@ impl CommitModal {
 
     fn toggle_branch_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         if self.branch_list_handle.is_focused(window, cx) {
-            self.focus_handle(cx).focus(window)
+            self.focus_handle(cx).focus(window, cx)
         } else {
             self.branch_list_handle.toggle(window, cx);
         }
@@ -578,8 +587,8 @@ impl Render for CommitModal {
                     .bg(cx.theme().colors().editor_background)
                     .border_1()
                     .border_color(cx.theme().colors().border_variant)
-                    .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
-                        window.focus(&editor_focus_handle);
+                    .on_click(cx.listener(move |_, _: &ClickEvent, window, cx| {
+                        window.focus(&editor_focus_handle, cx);
                     }))
                     .child(
                         div()

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

@@ -3,7 +3,7 @@ use editor::hover_markdown_style;
 use futures::Future;
 use git::blame::BlameEntry;
 use git::repository::CommitSummary;
-use git::{GitRemote, blame::ParsedCommitMessage};
+use git::{GitRemote, commit::ParsedCommitMessage};
 use gpui::{
     App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
     StatefulInteractiveElement, WeakEntity, prelude::*,

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

@@ -633,9 +633,9 @@ impl Item for FileHistoryView {
         &mut self,
         _workspace: &mut Workspace,
         window: &mut Window,
-        _cx: &mut Context<Self>,
+        cx: &mut Context<Self>,
     ) {
-        window.focus(&self.focus_handle);
+        window.focus(&self.focus_handle, cx);
     }
 
     fn show_toolbar(&self) -> bool {

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

@@ -15,12 +15,13 @@ use askpass::AskPassDelegate;
 use cloud_llm_client::CompletionIntent;
 use collections::{BTreeMap, HashMap, HashSet};
 use db::kvp::KEY_VALUE_STORE;
+use editor::RewrapOptions;
 use editor::{
     Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset,
     actions::ExpandAllDiffHunks,
 };
 use futures::StreamExt as _;
-use git::blame::ParsedCommitMessage;
+use git::commit::ParsedCommitMessage;
 use git::repository::{
     Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter,
     PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,
@@ -30,22 +31,22 @@ use git::stash::GitStash;
 use git::status::StageStatus;
 use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus};
 use git::{
-    ExpandCommitEditor, RestoreTrackedFiles, StageAll, StashAll, StashApply, StashPop,
-    TrashUntrackedFiles, UnstageAll,
+    ExpandCommitEditor, GitHostingProviderRegistry, RestoreTrackedFiles, StageAll, StashAll,
+    StashApply, StashPop, TrashUntrackedFiles, UnstageAll,
 };
 use gpui::{
     Action, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, Corner, DismissEvent, Entity,
-    EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
-    ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy,
-    Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point,
-    size, uniform_list,
+    EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent, Point,
+    PromptLevel, ScrollStrategy, Subscription, Task, UniformListScrollHandle, WeakEntity, actions,
+    anchored, deferred, point, size, uniform_list,
 };
 use itertools::Itertools;
 use language::{Buffer, File};
 use language_model::{
-    ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
+    ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
+    Role, ZED_CLOUD_PROVIDER_ID,
 };
-use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
+use menu;
 use multi_buffer::ExcerptInfo;
 use notifications::status_toast::{StatusToast, ToastIcon};
 use panel::{
@@ -57,7 +58,7 @@ use project::{
     git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},
     project_settings::{GitPathStyle, ProjectSettings},
 };
-use prompt_store::{PromptId, PromptStore, RULES_FILE_NAMES};
+use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES};
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore, StatusStyle};
 use std::future::Future;
@@ -92,6 +93,14 @@ actions!(
         FocusEditor,
         /// Focuses on the changes list.
         FocusChanges,
+        /// Select next git panel menu item, and show it in the diff view
+        NextEntry,
+        /// Select previous git panel menu item, and show it in the diff view
+        PreviousEntry,
+        /// Select first git panel menu item, and show it in the diff view
+        FirstEntry,
+        /// Select last git panel menu item, and show it in the diff view
+        LastEntry,
         /// Toggles automatic co-author suggestions.
         ToggleFillCoAuthors,
         /// Toggles sorting entries by path vs status.
@@ -203,8 +212,7 @@ const GIT_PANEL_KEY: &str = "GitPanel";
 
 const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
 // TODO: We should revise this part. It seems the indentation width is not aligned with the one in project panel
-const TREE_INDENT: f32 = 12.0;
-const TREE_INDENT_GUIDE_OFFSET: f32 = 16.0;
+const TREE_INDENT: f32 = 16.0;
 
 pub fn register(workspace: &mut Workspace) {
     workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
@@ -278,6 +286,13 @@ impl GitListEntry {
             _ => None,
         }
     }
+
+    fn directory_entry(&self) -> Option<&GitTreeDirEntry> {
+        match self {
+            GitListEntry::Directory(entry) => Some(entry),
+            _ => None,
+        }
+    }
 }
 
 enum GitPanelViewMode {
@@ -593,7 +608,7 @@ pub struct GitPanel {
     tracked_staged_count: usize,
     update_visible_entries_task: Task<()>,
     width: Option<Pixels>,
-    workspace: WeakEntity<Workspace>,
+    pub(crate) workspace: WeakEntity<Workspace>,
     context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
     modal_open: bool,
     show_placeholders: bool,
@@ -785,20 +800,63 @@ impl GitPanel {
     pub fn select_entry_by_path(
         &mut self,
         path: ProjectPath,
-        _: &mut Window,
+        window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         let Some(git_repo) = self.active_repository.as_ref() else {
             return;
         };
-        let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path, cx) else {
-            return;
+
+        let (repo_path, section) = {
+            let repo = git_repo.read(cx);
+            let Some(repo_path) = repo.project_path_to_repo_path(&path, cx) else {
+                return;
+            };
+
+            let section = repo
+                .status_for_path(&repo_path)
+                .map(|status| status.status)
+                .map(|status| {
+                    if repo.had_conflict_on_last_merge_head_change(&repo_path) {
+                        Section::Conflict
+                    } else if status.is_created() {
+                        Section::New
+                    } else {
+                        Section::Tracked
+                    }
+                });
+
+            (repo_path, section)
         };
+
+        let mut needs_rebuild = false;
+        if let (Some(section), Some(tree_state)) = (section, self.view_mode.tree_state_mut()) {
+            let mut current_dir = repo_path.parent();
+            while let Some(dir) = current_dir {
+                let key = TreeKey {
+                    section,
+                    path: RepoPath::from_rel_path(dir),
+                };
+
+                if tree_state.expanded_dirs.get(&key) == Some(&false) {
+                    tree_state.expanded_dirs.insert(key, true);
+                    needs_rebuild = true;
+                }
+
+                current_dir = dir.parent();
+            }
+        }
+
+        if needs_rebuild {
+            self.update_visible_entries(window, cx);
+        }
+
         let Some(ix) = self.entry_by_path(&repo_path) else {
             return;
         };
+
         self.selected_entry = Some(ix);
-        cx.notify();
+        self.scroll_to_selected_entry(cx);
     }
 
     fn serialization_key(workspace: &Workspace) -> Option<String> {
@@ -886,20 +944,27 @@ impl GitPanel {
     }
 
     fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) {
-        if let Some(selected_entry) = self.selected_entry {
+        let Some(selected_entry) = self.selected_entry else {
+            cx.notify();
+            return;
+        };
+
+        let visible_index = match &self.view_mode {
+            GitPanelViewMode::Flat => Some(selected_entry),
+            GitPanelViewMode::Tree(state) => state
+                .logical_indices
+                .iter()
+                .position(|&ix| ix == selected_entry),
+        };
+
+        if let Some(visible_index) = visible_index {
             self.scroll_handle
-                .scroll_to_item(selected_entry, ScrollStrategy::Center);
+                .scroll_to_item(visible_index, ScrollStrategy::Center);
         }
 
         cx.notify();
     }
 
-    fn first_status_entry_index(&self) -> Option<usize> {
-        self.entries
-            .iter()
-            .position(|entry| entry.status_entry().is_some())
-    }
-
     fn expand_selected_entry(
         &mut self,
         _: &ExpandSelectedEntry,
@@ -912,12 +977,12 @@ impl GitPanel {
 
         if let GitListEntry::Directory(dir_entry) = entry {
             if dir_entry.expanded {
-                self.select_next(&SelectNext, window, cx);
+                self.select_next(&menu::SelectNext, window, cx);
             } else {
                 self.toggle_directory(&dir_entry.key, window, cx);
             }
         } else {
-            self.select_next(&SelectNext, window, cx);
+            self.select_next(&menu::SelectNext, window, cx);
         }
     }
 
@@ -935,15 +1000,34 @@ impl GitPanel {
             if dir_entry.expanded {
                 self.toggle_directory(&dir_entry.key, window, cx);
             } else {
-                self.select_previous(&SelectPrevious, window, cx);
+                self.select_previous(&menu::SelectPrevious, window, cx);
             }
         } else {
-            self.select_previous(&SelectPrevious, window, cx);
+            self.select_previous(&menu::SelectPrevious, window, cx);
         }
     }
 
-    fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context<Self>) {
-        if let Some(first_entry) = self.first_status_entry_index() {
+    fn select_first(
+        &mut self,
+        _: &menu::SelectFirst,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let first_entry = match &self.view_mode {
+            GitPanelViewMode::Flat => self
+                .entries
+                .iter()
+                .position(|entry| entry.status_entry().is_some()),
+            GitPanelViewMode::Tree(state) => {
+                let index = self.entries.iter().position(|entry| {
+                    entry.status_entry().is_some() || entry.directory_entry().is_some()
+                });
+
+                index.map(|index| state.logical_indices[index])
+            }
+        };
+
+        if let Some(first_entry) = first_entry {
             self.selected_entry = Some(first_entry);
             self.scroll_to_selected_entry(cx);
         }
@@ -951,7 +1035,7 @@ impl GitPanel {
 
     fn select_previous(
         &mut self,
-        _: &SelectPrevious,
+        _: &menu::SelectPrevious,
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -960,80 +1044,142 @@ impl GitPanel {
             return;
         }
 
-        if let Some(selected_entry) = self.selected_entry {
-            let new_selected_entry = if selected_entry > 0 {
-                selected_entry - 1
-            } else {
-                selected_entry
-            };
+        let Some(selected_entry) = self.selected_entry else {
+            return;
+        };
 
-            if matches!(
-                self.entries.get(new_selected_entry),
-                Some(GitListEntry::Header(..))
-            ) {
-                if new_selected_entry > 0 {
-                    self.selected_entry = Some(new_selected_entry - 1)
-                }
-            } else {
-                self.selected_entry = Some(new_selected_entry);
+        let new_index = match &self.view_mode {
+            GitPanelViewMode::Flat => selected_entry.saturating_sub(1),
+            GitPanelViewMode::Tree(state) => {
+                let Some(current_logical_index) = state
+                    .logical_indices
+                    .iter()
+                    .position(|&i| i == selected_entry)
+                else {
+                    return;
+                };
+
+                state.logical_indices[current_logical_index.saturating_sub(1)]
             }
+        };
 
-            self.scroll_to_selected_entry(cx);
+        if selected_entry == 0 && new_index == 0 {
+            return;
         }
 
-        cx.notify();
+        if matches!(
+            self.entries.get(new_index.saturating_sub(1)),
+            Some(GitListEntry::Header(..))
+        ) && new_index == 0
+        {
+            return;
+        }
+
+        if matches!(self.entries.get(new_index), Some(GitListEntry::Header(..))) {
+            self.selected_entry = Some(new_index.saturating_sub(1));
+        } else {
+            self.selected_entry = Some(new_index);
+        }
+
+        self.scroll_to_selected_entry(cx);
     }
 
-    fn select_next(&mut self, _: &SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
+    fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
         let item_count = self.entries.len();
         if item_count == 0 {
             return;
         }
 
-        if let Some(selected_entry) = self.selected_entry {
-            let new_selected_entry = if selected_entry < item_count - 1 {
-                selected_entry + 1
-            } else {
-                selected_entry
-            };
-            if matches!(
-                self.entries.get(new_selected_entry),
-                Some(GitListEntry::Header(..))
-            ) {
-                self.selected_entry = Some(new_selected_entry + 1);
-            } else {
-                self.selected_entry = Some(new_selected_entry);
+        let Some(selected_entry) = self.selected_entry else {
+            return;
+        };
+
+        if selected_entry == item_count - 1 {
+            return;
+        }
+
+        let new_index = match &self.view_mode {
+            GitPanelViewMode::Flat => selected_entry.saturating_add(1),
+            GitPanelViewMode::Tree(state) => {
+                let Some(current_logical_index) = state
+                    .logical_indices
+                    .iter()
+                    .position(|&i| i == selected_entry)
+                else {
+                    return;
+                };
+
+                state.logical_indices[current_logical_index.saturating_add(1)]
             }
+        };
 
-            self.scroll_to_selected_entry(cx);
+        if matches!(self.entries.get(new_index), Some(GitListEntry::Header(..))) {
+            self.selected_entry = Some(new_index.saturating_add(1));
+        } else {
+            self.selected_entry = Some(new_index);
         }
 
-        cx.notify();
+        self.scroll_to_selected_entry(cx);
     }
 
-    fn select_last(&mut self, _: &SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
+    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
         if self.entries.last().is_some() {
             self.selected_entry = Some(self.entries.len() - 1);
             self.scroll_to_selected_entry(cx);
         }
     }
 
+    /// Show diff view at selected entry, only if the diff view is open
+    fn move_diff_to_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        maybe!({
+            let workspace = self.workspace.upgrade()?;
+
+            if let Some(project_diff) = workspace.read(cx).item_of_type::<ProjectDiff>(cx) {
+                let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
+
+                project_diff.update(cx, |project_diff, cx| {
+                    project_diff.move_to_entry(entry.clone(), window, cx);
+                });
+            }
+
+            Some(())
+        });
+    }
+
+    fn first_entry(&mut self, _: &FirstEntry, window: &mut Window, cx: &mut Context<Self>) {
+        self.select_first(&menu::SelectFirst, window, cx);
+        self.move_diff_to_entry(window, cx);
+    }
+
+    fn last_entry(&mut self, _: &LastEntry, window: &mut Window, cx: &mut Context<Self>) {
+        self.select_last(&menu::SelectLast, window, cx);
+        self.move_diff_to_entry(window, cx);
+    }
+
+    fn next_entry(&mut self, _: &NextEntry, window: &mut Window, cx: &mut Context<Self>) {
+        self.select_next(&menu::SelectNext, window, cx);
+        self.move_diff_to_entry(window, cx);
+    }
+
+    fn previous_entry(&mut self, _: &PreviousEntry, window: &mut Window, cx: &mut Context<Self>) {
+        self.select_previous(&menu::SelectPrevious, window, cx);
+        self.move_diff_to_entry(window, cx);
+    }
+
     fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
         self.commit_editor.update(cx, |editor, cx| {
-            window.focus(&editor.focus_handle(cx));
+            window.focus(&editor.focus_handle(cx), cx);
         });
         cx.notify();
     }
 
-    fn select_first_entry_if_none(&mut self, cx: &mut Context<Self>) {
+    fn select_first_entry_if_none(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let have_entries = self
             .active_repository
             .as_ref()
             .is_some_and(|active_repository| active_repository.read(cx).status_summary().count > 0);
         if have_entries && self.selected_entry.is_none() {
-            self.selected_entry = self.first_status_entry_index();
-            self.scroll_to_selected_entry(cx);
-            cx.notify();
+            self.select_first(&menu::SelectFirst, window, cx);
         }
     }
 
@@ -1043,10 +1189,8 @@ impl GitPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.select_first_entry_if_none(cx);
-
-        self.focus_handle.focus(window);
-        cx.notify();
+        self.focus_handle.focus(window, cx);
+        self.select_first_entry_if_none(window, cx);
     }
 
     fn get_selected_entry(&self) -> Option<&GitListEntry> {
@@ -1067,7 +1211,7 @@ impl GitPanel {
                         .project_path_to_repo_path(&project_path, cx)
                         .as_ref()
             {
-                project_diff.focus_handle(cx).focus(window);
+                project_diff.focus_handle(cx).focus(window, cx);
                 project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx));
                 return None;
             };
@@ -1077,7 +1221,7 @@ impl GitPanel {
                     ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
                 })
                 .ok();
-            self.focus_handle.focus(window);
+            self.focus_handle.focus(window, cx);
 
             Some(())
         });
@@ -1180,14 +1324,14 @@ impl GitPanel {
                 let prompt = window.prompt(
                     PromptLevel::Warning,
                     &format!(
-                        "Are you sure you want to restore {}?",
+                        "Are you sure you want to discard changes to {}?",
                         entry
                             .repo_path
                             .file_name()
                             .unwrap_or(entry.repo_path.display(path_style).as_ref()),
                     ),
                     None,
-                    &["Restore", "Cancel"],
+                    &["Discard Changes", "Cancel"],
                     cx,
                 );
                 cx.background_spawn(prompt)
@@ -2037,7 +2181,13 @@ impl GitPanel {
         let editor = cx.new(|cx| Editor::for_buffer(buffer, None, window, cx));
         let wrapped_message = editor.update(cx, |editor, cx| {
             editor.select_all(&Default::default(), window, cx);
-            editor.rewrap(&Default::default(), window, cx);
+            editor.rewrap_impl(
+                RewrapOptions {
+                    override_language_settings: false,
+                    preserve_existing_whitespace: true,
+                },
+                cx,
+            );
             editor.text(cx)
         });
         if wrapped_message.trim().is_empty() {
@@ -2088,7 +2238,10 @@ impl GitPanel {
         let commit_message = self.custom_or_suggested_commit_message(window, cx);
 
         let Some(mut message) = commit_message else {
-            self.commit_editor.read(cx).focus_handle(cx).focus(window);
+            self.commit_editor
+                .read(cx)
+                .focus_handle(cx)
+                .focus(window, cx);
             return;
         };
 
@@ -2422,18 +2575,30 @@ impl GitPanel {
         }
     }
 
-    async fn load_commit_message_prompt(cx: &mut AsyncApp) -> String {
-        const DEFAULT_PROMPT: &str = include_str!("commit_message_prompt.txt");
+    async fn load_commit_message_prompt(
+        is_using_legacy_zed_pro: bool,
+        cx: &mut AsyncApp,
+    ) -> String {
+        // Remove this once we stop supporting legacy Zed Pro
+        // In legacy Zed Pro, Git commit summary generation did not count as a
+        // prompt. If the user changes the prompt, our classification will fail,
+        // meaning that users will be charged for generating commit messages.
+        if is_using_legacy_zed_pro {
+            return BuiltInPrompt::CommitMessage.default_content().to_string();
+        }
 
         let load = async {
             let store = cx.update(|cx| PromptStore::global(cx)).ok()?.await.ok()?;
             store
-                .update(cx, |s, cx| s.load(PromptId::CommitMessage, cx))
+                .update(cx, |s, cx| {
+                    s.load(PromptId::BuiltIn(BuiltInPrompt::CommitMessage), cx)
+                })
                 .ok()?
                 .await
                 .ok()
         };
-        load.await.unwrap_or_else(|| DEFAULT_PROMPT.to_string())
+        load.await
+            .unwrap_or_else(|| BuiltInPrompt::CommitMessage.default_content().to_string())
     }
 
     /// Generates a commit message using an LLM.
@@ -2466,6 +2631,13 @@ impl GitPanel {
         let project = self.project.clone();
         let repo_work_dir = repo.read(cx).work_directory_abs_path.clone();
 
+        // Remove this once we stop supporting legacy Zed Pro
+        let is_using_legacy_zed_pro = provider.id() == ZED_CLOUD_PROVIDER_ID
+            && self.workspace.upgrade().map_or(false, |workspace| {
+                workspace.read(cx).user_store().read(cx).plan()
+                    == Some(cloud_llm_client::Plan::V1(cloud_llm_client::PlanV1::ZedPro))
+            });
+
         self.generate_commit_message_task = Some(cx.spawn(async move |this, mut cx| {
              async move {
                 let _defer = cx.on_drop(&this, |this, _cx| {
@@ -2501,7 +2673,7 @@ impl GitPanel {
 
                 let rules_content = Self::load_project_rules(&project, &repo_work_dir, &mut cx).await;
 
-                let prompt = Self::load_commit_message_prompt(&mut cx).await;
+                let prompt = Self::load_commit_message_prompt(is_using_legacy_zed_pro, &mut cx).await;
 
                 let subject = this.update(cx, |this, cx| {
                     this.commit_editor.read(cx).text(cx).lines().next().map(ToOwned::to_owned).unwrap_or_default()
@@ -3503,7 +3675,7 @@ impl GitPanel {
             self.bulk_staging = bulk_staging;
         }
 
-        self.select_first_entry_if_none(cx);
+        self.select_first_entry_if_none(window, cx);
 
         let suggested_commit_message = self.suggest_commit_message(cx);
         let placeholder_text = suggested_commit_message.unwrap_or("Enter commit message".into());
@@ -4088,7 +4260,7 @@ impl GitPanel {
                     .border_color(cx.theme().colors().border)
                     .cursor_text()
                     .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
-                        window.focus(&this.commit_editor.focus_handle(cx));
+                        window.focus(&this.commit_editor.focus_handle(cx), cx);
                     }))
                     .child(
                         h_flex()
@@ -4531,7 +4703,10 @@ impl GitPanel {
                                         },
                                     )
                                     .with_render_fn(cx.entity(), |_, params, _, _| {
-                                        let left_offset = px(TREE_INDENT_GUIDE_OFFSET);
+                                        // Magic number to align the tree item is 3 here
+                                        // because we're using 12px as the left-side padding
+                                        // and 3 makes the alignment work with the bounding box of the icon
+                                        let left_offset = px(TREE_INDENT + 3_f32);
                                         let indent_size = params.indent_size;
                                         let item_height = params.item_height;
 
@@ -4559,10 +4734,6 @@ impl GitPanel {
                         })
                         .size_full()
                         .flex_grow()
-                        .with_sizing_behavior(ListSizingBehavior::Auto)
-                        .with_horizontal_sizing_behavior(
-                            ListHorizontalSizingBehavior::Unconstrained,
-                        )
                         .with_width_from_item(self.max_width_item_index)
                         .track_scroll(&self.scroll_handle),
                     )
@@ -4586,7 +4757,7 @@ impl GitPanel {
     }
 
     fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label {
-        Label::new(label.into()).color(color).single_line()
+        Label::new(label.into()).color(color)
     }
 
     fn list_item_height(&self) -> Rems {
@@ -4608,8 +4779,8 @@ impl GitPanel {
             .h(self.list_item_height())
             .w_full()
             .items_end()
-            .px(rems(0.75)) // ~12px
-            .pb(rems(0.3125)) // ~ 5px
+            .px_3()
+            .pb_1()
             .child(
                 Label::new(header.title())
                     .color(Color::Muted)
@@ -4652,7 +4823,7 @@ impl GitPanel {
         let restore_title = if entry.status.is_created() {
             "Trash File"
         } else {
-            "Restore File"
+            "Discard Changes"
         };
         let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
             let is_created = entry.status.is_created();
@@ -4666,8 +4837,8 @@ impl GitPanel {
                     git::AddToGitignore.boxed_clone(),
                 )
                 .separator()
-                .action("Open Diff", Confirm.boxed_clone())
-                .action("Open File", SecondaryConfirm.boxed_clone())
+                .action("Open Diff", menu::Confirm.boxed_clone())
+                .action("Open File", menu::SecondaryConfirm.boxed_clone())
                 .separator()
                 .action_disabled_when(is_created, "View File History", Box::new(git::FileHistory))
         });
@@ -4797,112 +4968,67 @@ impl GitPanel {
         let marked_bg_alpha = 0.12;
         let state_opacity_step = 0.04;
 
+        let info_color = cx.theme().status().info;
+
         let base_bg = match (selected, marked) {
-            (true, true) => cx
-                .theme()
-                .status()
-                .info
-                .alpha(selected_bg_alpha + marked_bg_alpha),
-            (true, false) => cx.theme().status().info.alpha(selected_bg_alpha),
-            (false, true) => cx.theme().status().info.alpha(marked_bg_alpha),
+            (true, true) => info_color.alpha(selected_bg_alpha + marked_bg_alpha),
+            (true, false) => info_color.alpha(selected_bg_alpha),
+            (false, true) => info_color.alpha(marked_bg_alpha),
             _ => cx.theme().colors().ghost_element_background,
         };
 
-        let hover_bg = if selected {
-            cx.theme()
-                .status()
-                .info
-                .alpha(selected_bg_alpha + state_opacity_step)
-        } else {
-            cx.theme().colors().ghost_element_hover
-        };
-
-        let active_bg = if selected {
-            cx.theme()
-                .status()
-                .info
-                .alpha(selected_bg_alpha + state_opacity_step * 2.0)
+        let (hover_bg, active_bg) = if selected {
+            (
+                info_color.alpha(selected_bg_alpha + state_opacity_step),
+                info_color.alpha(selected_bg_alpha + state_opacity_step * 2.0),
+            )
         } else {
-            cx.theme().colors().ghost_element_active
+            (
+                cx.theme().colors().ghost_element_hover,
+                cx.theme().colors().ghost_element_active,
+            )
         };
 
-        let mut name_row = h_flex()
-            .items_center()
-            .gap_1()
+        let name_row = h_flex()
+            .min_w_0()
             .flex_1()
-            .pl(if tree_view {
-                px(depth as f32 * TREE_INDENT)
-            } else {
-                px(0.)
-            })
-            .child(git_status_icon(status));
-
-        name_row = if tree_view {
-            name_row.child(
-                self.entry_label(display_name, label_color)
-                    .when(status.is_deleted(), Label::strikethrough)
-                    .truncate(),
-            )
-        } else {
-            name_row.child(h_flex().items_center().flex_1().map(|this| {
-                self.path_formatted(
-                    this,
-                    entry.parent_dir(path_style),
-                    path_color,
-                    display_name,
-                    label_color,
-                    path_style,
-                    git_path_style,
-                    status.is_deleted(),
-                )
-            }))
-        };
+            .gap_1()
+            .child(git_status_icon(status))
+            .map(|this| {
+                if tree_view {
+                    this.pl(px(depth as f32 * TREE_INDENT)).child(
+                        self.entry_label(display_name, label_color)
+                            .when(status.is_deleted(), Label::strikethrough)
+                            .truncate(),
+                    )
+                } else {
+                    this.child(self.path_formatted(
+                        entry.parent_dir(path_style),
+                        path_color,
+                        display_name,
+                        label_color,
+                        path_style,
+                        git_path_style,
+                        status.is_deleted(),
+                    ))
+                }
+            });
 
         h_flex()
             .id(id)
             .h(self.list_item_height())
             .w_full()
+            .pl_3()
+            .pr_1()
+            .gap_1p5()
             .border_1()
             .border_r_2()
             .when(selected && self.focus_handle.is_focused(window), |el| {
                 el.border_color(cx.theme().colors().panel_focused_border)
             })
-            .px(rems(0.75)) // ~12px
-            .overflow_hidden()
-            .flex_none()
-            .gap_1p5()
             .bg(base_bg)
-            .hover(|this| this.bg(hover_bg))
-            .active(|this| this.bg(active_bg))
-            .on_click({
-                cx.listener(move |this, event: &ClickEvent, window, cx| {
-                    this.selected_entry = Some(ix);
-                    cx.notify();
-                    if event.modifiers().secondary() {
-                        this.open_file(&Default::default(), window, cx)
-                    } else {
-                        this.open_diff(&Default::default(), window, cx);
-                        this.focus_handle.focus(window);
-                    }
-                })
-            })
-            .on_mouse_down(
-                MouseButton::Right,
-                move |event: &MouseDownEvent, window, cx| {
-                    // why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`?
-                    if event.button != MouseButton::Right {
-                        return;
-                    }
-
-                    let Some(this) = handle.upgrade() else {
-                        return;
-                    };
-                    this.update(cx, |this, cx| {
-                        this.deploy_entry_context_menu(event.position, ix, window, cx);
-                    });
-                    cx.stop_propagation();
-                },
-            )
+            .hover(|s| s.bg(hover_bg))
+            .active(|s| s.bg(active_bg))
             .child(name_row)
             .child(
                 div()
@@ -4953,6 +5079,35 @@ impl GitPanel {
                             }),
                     ),
             )
+            .on_click({
+                cx.listener(move |this, event: &ClickEvent, window, cx| {
+                    this.selected_entry = Some(ix);
+                    cx.notify();
+                    if event.modifiers().secondary() {
+                        this.open_file(&Default::default(), window, cx)
+                    } else {
+                        this.open_diff(&Default::default(), window, cx);
+                        this.focus_handle.focus(window, cx);
+                    }
+                })
+            })
+            .on_mouse_down(
+                MouseButton::Right,
+                move |event: &MouseDownEvent, window, cx| {
+                    // why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`?
+                    if event.button != MouseButton::Right {
+                        return;
+                    }
+
+                    let Some(this) = handle.upgrade() else {
+                        return;
+                    };
+                    this.update(cx, |this, cx| {
+                        this.deploy_entry_context_menu(event.position, ix, window, cx);
+                    });
+                    cx.stop_propagation();
+                },
+            )
             .into_any_element()
     }
 
@@ -4977,29 +5132,23 @@ impl GitPanel {
         let selected_bg_alpha = 0.08;
         let state_opacity_step = 0.04;
 
-        let base_bg = if selected {
-            cx.theme().status().info.alpha(selected_bg_alpha)
-        } else {
-            cx.theme().colors().ghost_element_background
-        };
+        let info_color = cx.theme().status().info;
+        let colors = cx.theme().colors();
 
-        let hover_bg = if selected {
-            cx.theme()
-                .status()
-                .info
-                .alpha(selected_bg_alpha + state_opacity_step)
+        let (base_bg, hover_bg, active_bg) = if selected {
+            (
+                info_color.alpha(selected_bg_alpha),
+                info_color.alpha(selected_bg_alpha + state_opacity_step),
+                info_color.alpha(selected_bg_alpha + state_opacity_step * 2.0),
+            )
         } else {
-            cx.theme().colors().ghost_element_hover
+            (
+                colors.ghost_element_background,
+                colors.ghost_element_hover,
+                colors.ghost_element_active,
+            )
         };
 
-        let active_bg = if selected {
-            cx.theme()
-                .status()
-                .info
-                .alpha(selected_bg_alpha + state_opacity_step * 2.0)
-        } else {
-            cx.theme().colors().ghost_element_active
-        };
         let folder_icon = if entry.expanded {
             IconName::FolderOpen
         } else {
@@ -5022,9 +5171,8 @@ impl GitPanel {
         };
 
         let name_row = h_flex()
-            .items_center()
+            .min_w_0()
             .gap_1()
-            .flex_1()
             .pl(px(entry.depth as f32 * TREE_INDENT))
             .child(
                 Icon::new(folder_icon)
@@ -5036,27 +5184,20 @@ impl GitPanel {
         h_flex()
             .id(id)
             .h(self.list_item_height())
+            .min_w_0()
             .w_full()
-            .items_center()
+            .pl_3()
+            .pr_1()
+            .gap_1p5()
+            .justify_between()
             .border_1()
             .border_r_2()
             .when(selected && self.focus_handle.is_focused(window), |el| {
                 el.border_color(cx.theme().colors().panel_focused_border)
             })
-            .px(rems(0.75))
-            .overflow_hidden()
-            .flex_none()
-            .gap_1p5()
             .bg(base_bg)
-            .hover(|this| this.bg(hover_bg))
-            .active(|this| this.bg(active_bg))
-            .on_click({
-                let key = entry.key.clone();
-                cx.listener(move |this, _event: &ClickEvent, window, cx| {
-                    this.selected_entry = Some(ix);
-                    this.toggle_directory(&key, window, cx);
-                })
-            })
+            .hover(|s| s.bg(hover_bg))
+            .active(|s| s.bg(active_bg))
             .child(name_row)
             .child(
                 div()
@@ -5096,12 +5237,18 @@ impl GitPanel {
                             }),
                     ),
             )
+            .on_click({
+                let key = entry.key.clone();
+                cx.listener(move |this, _event: &ClickEvent, window, cx| {
+                    this.selected_entry = Some(ix);
+                    this.toggle_directory(&key, window, cx);
+                })
+            })
             .into_any_element()
     }
 
     fn path_formatted(
         &self,
-        parent: Div,
         directory: Option<String>,
         path_color: Color,
         file_name: String,
@@ -5110,41 +5257,31 @@ impl GitPanel {
         git_path_style: GitPathStyle,
         strikethrough: bool,
     ) -> Div {
-        parent
-            .when(git_path_style == GitPathStyle::FileNameFirst, |this| {
-                this.child(
-                    self.entry_label(
-                        match directory.as_ref().is_none_or(|d| d.is_empty()) {
-                            true => file_name.clone(),
-                            false => format!("{file_name} "),
-                        },
-                        label_color,
-                    )
-                    .when(strikethrough, Label::strikethrough),
-                )
-            })
-            .when_some(directory, |this, dir| {
-                match (
-                    !dir.is_empty(),
-                    git_path_style == GitPathStyle::FileNameFirst,
-                ) {
-                    (true, true) => this.child(
-                        self.entry_label(dir, path_color)
-                            .when(strikethrough, Label::strikethrough),
-                    ),
-                    (true, false) => this.child(
-                        self.entry_label(
-                            format!("{dir}{}", path_style.primary_separator()),
-                            path_color,
-                        )
+        let file_name_first = git_path_style == GitPathStyle::FileNameFirst;
+        let file_path_first = git_path_style == GitPathStyle::FilePathFirst;
+
+        let file_name = format!("{} ", file_name);
+
+        h_flex()
+            .min_w_0()
+            .overflow_hidden()
+            .when(file_path_first, |this| this.flex_row_reverse())
+            .child(
+                div().flex_none().child(
+                    self.entry_label(file_name, label_color)
                         .when(strikethrough, Label::strikethrough),
-                    ),
-                    _ => this,
-                }
-            })
-            .when(git_path_style == GitPathStyle::FilePathFirst, |this| {
+                ),
+            )
+            .when_some(directory, |this, dir| {
+                let path_name = if file_name_first {
+                    dir
+                } else {
+                    format!("{dir}{}", path_style.primary_separator())
+                };
+
                 this.child(
-                    self.entry_label(file_name, label_color)
+                    self.entry_label(path_name, path_color)
+                        .truncate()
                         .when(strikethrough, Label::strikethrough),
                 )
             })

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

@@ -817,7 +817,7 @@ impl GitCloneModal {
         });
         let focus_handle = repo_input.focus_handle(cx);
 
-        window.focus(&focus_handle);
+        window.focus(&focus_handle, cx);
 
         Self {
             panel,

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

@@ -85,8 +85,8 @@ impl Render for GitOnboardingModal {
                 git_onboarding_event!("Cancelled", trigger = "Action");
                 cx.emit(DismissEvent);
             }))
-            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
-                this.focus_handle.focus(window);
+            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
+                this.focus_handle.focus(window, cx);
             }))
             .child(
                 div().p_1p5().absolute().inset_0().h(px(160.)).child(

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

@@ -492,7 +492,7 @@ impl ProjectDiff {
         if editor.focus_handle(cx).contains_focused(window, cx)
             && self.multibuffer.read(cx).is_empty()
         {
-            self.focus_handle.focus(window)
+            self.focus_handle.focus(window, cx)
         }
     }
 
@@ -597,10 +597,10 @@ impl ProjectDiff {
                 .focus_handle(cx)
                 .contains_focused(window, cx)
         {
-            self.focus_handle.focus(window);
+            self.focus_handle.focus(window, cx);
         } else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
             self.editor.update(cx, |editor, cx| {
-                editor.focus_handle(cx).focus(window);
+                editor.focus_handle(cx).focus(window, cx);
             });
         }
         if self.pending_scroll.as_ref() == Some(&path_key) {
@@ -983,7 +983,7 @@ impl Render for ProjectDiff {
                                         cx,
                                     ))
                                     .on_click(move |_, window, cx| {
-                                        window.focus(&keybinding_focus_handle);
+                                        window.focus(&keybinding_focus_handle, cx);
                                         window.dispatch_action(
                                             Box::new(CloseActiveItem::default()),
                                             cx,
@@ -1153,7 +1153,7 @@ impl ProjectDiffToolbar {
 
     fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
         if let Some(project_diff) = self.project_diff(cx) {
-            project_diff.focus_handle(cx).focus(window);
+            project_diff.focus_handle(cx).focus(window, cx);
         }
         let action = action.boxed_clone();
         cx.defer(move |cx| {

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

@@ -1,4 +1,5 @@
 use anyhow::Context as _;
+use collections::HashSet;
 use fuzzy::StringMatchCandidate;
 
 use git::repository::Worktree as GitWorktree;
@@ -9,7 +10,11 @@ use gpui::{
     actions, rems,
 };
 use picker::{Picker, PickerDelegate, PickerEditorPosition};
-use project::{DirectoryLister, git_store::Repository};
+use project::{
+    DirectoryLister,
+    git_store::Repository,
+    trusted_worktrees::{PathTrust, RemoteHostLocation, TrustedWorktrees},
+};
 use recent_projects::{RemoteConnectionModal, connect};
 use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};
 use std::{path::PathBuf, sync::Arc};
@@ -219,7 +224,6 @@ impl WorktreeListDelegate {
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) {
-        let workspace = self.workspace.clone();
         let Some(repo) = self.repo.clone() else {
             return;
         };
@@ -247,6 +251,7 @@ impl WorktreeListDelegate {
 
         let branch = worktree_branch.to_string();
         let window_handle = window.window_handle();
+        let workspace = self.workspace.clone();
         cx.spawn_in(window, async move |_, cx| {
             let Some(paths) = worktree_path.await? else {
                 return anyhow::Ok(());
@@ -257,8 +262,32 @@ impl WorktreeListDelegate {
                 repo.create_worktree(branch.clone(), path.clone(), commit)
             })?
             .await??;
-
-            let final_path = path.join(branch);
+            let new_worktree_path = path.join(branch);
+
+            workspace.update(cx, |workspace, cx| {
+                if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
+                    let repo_path = &repo.read(cx).snapshot().work_directory_abs_path;
+                    let project = workspace.project();
+                    if let Some((parent_worktree, _)) =
+                        project.read(cx).find_worktree(repo_path, cx)
+                    {
+                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
+                            if trusted_worktrees.can_trust(parent_worktree.read(cx).id(), cx) {
+                                trusted_worktrees.trust(
+                                    HashSet::from_iter([PathTrust::AbsPath(
+                                        new_worktree_path.clone(),
+                                    )]),
+                                    project
+                                        .read(cx)
+                                        .remote_connection_options(cx)
+                                        .map(RemoteHostLocation::from),
+                                    cx,
+                                );
+                            }
+                        });
+                    }
+                }
+            })?;
 
             let (connection_options, app_state, is_local) =
                 workspace.update(cx, |workspace, cx| {
@@ -274,7 +303,7 @@ impl WorktreeListDelegate {
                     .update_in(cx, |workspace, window, cx| {
                         workspace.open_workspace_for_paths(
                             replace_current_window,
-                            vec![final_path],
+                            vec![new_worktree_path],
                             window,
                             cx,
                         )
@@ -283,7 +312,7 @@ impl WorktreeListDelegate {
             } else if let Some(connection_options) = connection_options {
                 open_remote_worktree(
                     connection_options,
-                    vec![final_path],
+                    vec![new_worktree_path],
                     app_state,
                     window_handle,
                     replace_current_window,

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

@@ -268,7 +268,7 @@ impl GoToLine {
                 cx,
                 |s| s.select_anchor_ranges([start..start]),
             );
-            editor.focus_handle(cx).focus(window);
+            editor.focus_handle(cx).focus(window, cx);
             cx.notify()
         });
         self.prev_scroll_position.take();

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

@@ -512,6 +512,8 @@ pub enum Model {
     Gemini25Pro,
     #[serde(rename = "gemini-3-pro-preview")]
     Gemini3Pro,
+    #[serde(rename = "gemini-3-flash-preview")]
+    Gemini3Flash,
     #[serde(rename = "custom")]
     Custom {
         name: String,
@@ -534,6 +536,7 @@ impl Model {
             Self::Gemini25Flash => "gemini-2.5-flash",
             Self::Gemini25Pro => "gemini-2.5-pro",
             Self::Gemini3Pro => "gemini-3-pro-preview",
+            Self::Gemini3Flash => "gemini-3-flash-preview",
             Self::Custom { name, .. } => name,
         }
     }
@@ -543,6 +546,7 @@ impl Model {
             Self::Gemini25Flash => "gemini-2.5-flash",
             Self::Gemini25Pro => "gemini-2.5-pro",
             Self::Gemini3Pro => "gemini-3-pro-preview",
+            Self::Gemini3Flash => "gemini-3-flash-preview",
             Self::Custom { name, .. } => name,
         }
     }
@@ -553,6 +557,7 @@ impl Model {
             Self::Gemini25Flash => "Gemini 2.5 Flash",
             Self::Gemini25Pro => "Gemini 2.5 Pro",
             Self::Gemini3Pro => "Gemini 3 Pro",
+            Self::Gemini3Flash => "Gemini 3 Flash",
             Self::Custom {
                 name, display_name, ..
             } => display_name.as_ref().unwrap_or(name),
@@ -561,20 +566,22 @@ impl Model {
 
     pub fn max_token_count(&self) -> u64 {
         match self {
-            Self::Gemini25FlashLite => 1_048_576,
-            Self::Gemini25Flash => 1_048_576,
-            Self::Gemini25Pro => 1_048_576,
-            Self::Gemini3Pro => 1_048_576,
+            Self::Gemini25FlashLite
+            | Self::Gemini25Flash
+            | Self::Gemini25Pro
+            | Self::Gemini3Pro
+            | Self::Gemini3Flash => 1_048_576,
             Self::Custom { max_tokens, .. } => *max_tokens,
         }
     }
 
     pub fn max_output_tokens(&self) -> Option<u64> {
         match self {
-            Model::Gemini25FlashLite => Some(65_536),
-            Model::Gemini25Flash => Some(65_536),
-            Model::Gemini25Pro => Some(65_536),
-            Model::Gemini3Pro => Some(65_536),
+            Model::Gemini25FlashLite
+            | Model::Gemini25Flash
+            | Model::Gemini25Pro
+            | Model::Gemini3Pro
+            | Model::Gemini3Flash => Some(65_536),
             Model::Custom { .. } => None,
         }
     }
@@ -599,6 +606,7 @@ impl Model {
                     budget_tokens: None,
                 }
             }
+            Self::Gemini3Flash => GoogleModelMode::Default,
             Self::Custom { mode, .. } => *mode,
         }
     }

crates/gpui/Cargo.toml πŸ”—

@@ -198,14 +198,14 @@ wayland-backend = { version = "0.3.3", features = [
     "client_system",
     "dlopen",
 ], optional = true }
-wayland-client = { version = "0.31.2", optional = true }
-wayland-cursor = { version = "0.31.1", optional = true }
-wayland-protocols = { version = "0.31.2", features = [
+wayland-client = { version = "0.31.11", optional = true }
+wayland-cursor = { version = "0.31.11", optional = true }
+wayland-protocols = { version = "0.32.9", features = [
     "client",
     "staging",
     "unstable",
 ], optional = true }
-wayland-protocols-plasma = { version = "0.2.0", features = [
+wayland-protocols-plasma = { version = "0.3.9", features = [
     "client",
 ], optional = true }
 wayland-protocols-wlr = { version = "0.3.9", features = [

crates/gpui/examples/focus_visible.rs πŸ”—

@@ -29,7 +29,7 @@ impl Example {
         ];
 
         let focus_handle = cx.focus_handle();
-        window.focus(&focus_handle);
+        window.focus(&focus_handle, cx);
 
         Self {
             focus_handle,
@@ -40,13 +40,13 @@ impl Example {
         }
     }
 
-    fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
-        window.focus_next();
+    fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
+        window.focus_next(cx);
         self.message = SharedString::from("Pressed Tab - focus-visible border should appear!");
     }
 
-    fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
-        window.focus_prev();
+    fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
+        window.focus_prev(cx);
         self.message =
             SharedString::from("Pressed Shift-Tab - focus-visible border should appear!");
     }

crates/gpui/examples/input.rs πŸ”—

@@ -736,7 +736,7 @@ fn main() {
 
         window
             .update(cx, |view, window, cx| {
-                window.focus(&view.text_input.focus_handle(cx));
+                window.focus(&view.text_input.focus_handle(cx), cx);
                 cx.activate(true);
             })
             .unwrap();

crates/gpui/examples/on_window_close_quit.rs πŸ”—

@@ -55,7 +55,7 @@ fn main() {
                 cx.activate(false);
                 cx.new(|cx| {
                     let focus_handle = cx.focus_handle();
-                    focus_handle.focus(window);
+                    focus_handle.focus(window, cx);
                     ExampleWindow { focus_handle }
                 })
             },
@@ -72,7 +72,7 @@ fn main() {
             |window, cx| {
                 cx.new(|cx| {
                     let focus_handle = cx.focus_handle();
-                    focus_handle.focus(window);
+                    focus_handle.focus(window, cx);
                     ExampleWindow { focus_handle }
                 })
             },

crates/gpui/examples/tab_stop.rs πŸ”—

@@ -22,7 +22,7 @@ impl Example {
         ];
 
         let focus_handle = cx.focus_handle();
-        window.focus(&focus_handle);
+        window.focus(&focus_handle, cx);
 
         Self {
             focus_handle,
@@ -31,13 +31,13 @@ impl Example {
         }
     }
 
-    fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) {
-        window.focus_next();
+    fn on_tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
+        window.focus_next(cx);
         self.message = SharedString::from("You have pressed `Tab`.");
     }
 
-    fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) {
-        window.focus_prev();
+    fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, cx: &mut Context<Self>) {
+        window.focus_prev(cx);
         self.message = SharedString::from("You have pressed `Shift-Tab`.");
     }
 }

crates/gpui/examples/window.rs πŸ”—

@@ -5,6 +5,7 @@ use gpui::{
 
 struct SubWindow {
     custom_titlebar: bool,
+    is_dialog: bool,
 }
 
 fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> impl IntoElement {
@@ -23,7 +24,10 @@ fn button(text: &str, on_click: impl Fn(&mut Window, &mut App) + 'static) -> imp
 }
 
 impl Render for SubWindow {
-    fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let window_bounds =
+            WindowBounds::Windowed(Bounds::centered(None, size(px(250.0), px(200.0)), cx));
+
         div()
             .flex()
             .flex_col()
@@ -52,8 +56,28 @@ impl Render for SubWindow {
             .child(
                 div()
                     .p_8()
+                    .flex()
+                    .flex_col()
                     .gap_2()
                     .child("SubWindow")
+                    .when(self.is_dialog, |div| {
+                        div.child(button("Open Nested Dialog", move |_, cx| {
+                            cx.open_window(
+                                WindowOptions {
+                                    window_bounds: Some(window_bounds),
+                                    kind: WindowKind::Dialog,
+                                    ..Default::default()
+                                },
+                                |_, cx| {
+                                    cx.new(|_| SubWindow {
+                                        custom_titlebar: false,
+                                        is_dialog: true,
+                                    })
+                                },
+                            )
+                            .unwrap();
+                        }))
+                    })
                     .child(button("Close", |window, _| {
                         window.remove_window();
                     })),
@@ -86,6 +110,7 @@ impl Render for WindowDemo {
                     |_, cx| {
                         cx.new(|_| SubWindow {
                             custom_titlebar: false,
+                            is_dialog: false,
                         })
                     },
                 )
@@ -101,6 +126,39 @@ impl Render for WindowDemo {
                     |_, cx| {
                         cx.new(|_| SubWindow {
                             custom_titlebar: false,
+                            is_dialog: false,
+                        })
+                    },
+                )
+                .unwrap();
+            }))
+            .child(button("Floating", move |_, cx| {
+                cx.open_window(
+                    WindowOptions {
+                        window_bounds: Some(window_bounds),
+                        kind: WindowKind::Floating,
+                        ..Default::default()
+                    },
+                    |_, cx| {
+                        cx.new(|_| SubWindow {
+                            custom_titlebar: false,
+                            is_dialog: false,
+                        })
+                    },
+                )
+                .unwrap();
+            }))
+            .child(button("Dialog", move |_, cx| {
+                cx.open_window(
+                    WindowOptions {
+                        window_bounds: Some(window_bounds),
+                        kind: WindowKind::Dialog,
+                        ..Default::default()
+                    },
+                    |_, cx| {
+                        cx.new(|_| SubWindow {
+                            custom_titlebar: false,
+                            is_dialog: true,
                         })
                     },
                 )
@@ -116,6 +174,7 @@ impl Render for WindowDemo {
                     |_, cx| {
                         cx.new(|_| SubWindow {
                             custom_titlebar: true,
+                            is_dialog: false,
                         })
                     },
                 )
@@ -131,6 +190,7 @@ impl Render for WindowDemo {
                     |_, cx| {
                         cx.new(|_| SubWindow {
                             custom_titlebar: false,
+                            is_dialog: false,
                         })
                     },
                 )
@@ -147,6 +207,7 @@ impl Render for WindowDemo {
                     |_, cx| {
                         cx.new(|_| SubWindow {
                             custom_titlebar: false,
+                            is_dialog: false,
                         })
                     },
                 )
@@ -162,6 +223,7 @@ impl Render for WindowDemo {
                     |_, cx| {
                         cx.new(|_| SubWindow {
                             custom_titlebar: false,
+                            is_dialog: false,
                         })
                     },
                 )
@@ -177,6 +239,7 @@ impl Render for WindowDemo {
                     |_, cx| {
                         cx.new(|_| SubWindow {
                             custom_titlebar: false,
+                            is_dialog: false,
                         })
                     },
                 )

crates/gpui/src/app.rs πŸ”—

@@ -316,6 +316,7 @@ impl SystemWindowTabController {
             .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
 
         let current_group = current_group?;
+        // TODO: `.keys()` returns arbitrary order, what does "next" mean?
         let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
         let idx = group_ids.iter().position(|g| *g == current_group)?;
         let next_idx = (idx + 1) % group_ids.len();
@@ -340,6 +341,7 @@ impl SystemWindowTabController {
             .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
 
         let current_group = current_group?;
+        // TODO: `.keys()` returns arbitrary order, what does "previous" mean?
         let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
         let idx = group_ids.iter().position(|g| *g == current_group)?;
         let prev_idx = if idx == 0 {
@@ -361,12 +363,9 @@ impl SystemWindowTabController {
 
     /// Get all tabs in the same window.
     pub fn tabs(&self, id: WindowId) -> Option<&Vec<SystemWindowTab>> {
-        let tab_group = self
-            .tab_groups
-            .iter()
-            .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group))?;
-
-        self.tab_groups.get(&tab_group)
+        self.tab_groups
+            .values()
+            .find(|tabs| tabs.iter().any(|tab| tab.id == id))
     }
 
     /// Initialize the visibility of the system window tab controller.
@@ -441,7 +440,7 @@ impl SystemWindowTabController {
     /// Insert a tab into a tab group.
     pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec<SystemWindowTab>) {
         let mut controller = cx.global_mut::<SystemWindowTabController>();
-        let Some(tab) = tabs.clone().into_iter().find(|tab| tab.id == id) else {
+        let Some(tab) = tabs.iter().find(|tab| tab.id == id).cloned() else {
             return;
         };
 
@@ -504,16 +503,14 @@ impl SystemWindowTabController {
             return;
         };
 
+        let initial_tabs_len = initial_tabs.len();
         let mut all_tabs = initial_tabs.clone();
-        for tabs in controller.tab_groups.values() {
-            all_tabs.extend(
-                tabs.iter()
-                    .filter(|tab| !initial_tabs.contains(tab))
-                    .cloned(),
-            );
+
+        for (_, mut tabs) in controller.tab_groups.drain() {
+            tabs.retain(|tab| !all_tabs[..initial_tabs_len].contains(tab));
+            all_tabs.extend(tabs);
         }
 
-        controller.tab_groups.clear();
         controller.tab_groups.insert(0, all_tabs);
     }
 
@@ -1900,8 +1897,11 @@ impl App {
     pub(crate) fn clear_pending_keystrokes(&mut self) {
         for window in self.windows() {
             window
-                .update(self, |_, window, _| {
-                    window.clear_pending_keystrokes();
+                .update(self, |_, window, cx| {
+                    if window.pending_input_keystrokes().is_some() {
+                        window.clear_pending_keystrokes();
+                        window.pending_input_changed(cx);
+                    }
                 })
                 .ok();
         }

crates/gpui/src/app/async_context.rs πŸ”—

@@ -487,7 +487,7 @@ impl VisualContext for AsyncWindowContext {
         V: Focusable,
     {
         self.app.update_window(self.window, |_, window, cx| {
-            view.read(cx).focus_handle(cx).focus(window);
+            view.read(cx).focus_handle(cx).focus(window, cx);
         })
     }
 }

crates/gpui/src/app/context.rs πŸ”—

@@ -285,7 +285,7 @@ impl<'a, T: 'static> Context<'a, T> {
 
     /// Focus the given view in the given window. View type is required to implement Focusable.
     pub fn focus_view<W: Focusable>(&mut self, view: &Entity<W>, window: &mut Window) {
-        window.focus(&view.focus_handle(self));
+        window.focus(&view.focus_handle(self), self);
     }
 
     /// Sets a given callback to be run on the next frame.
@@ -732,7 +732,7 @@ impl<'a, T: 'static> Context<'a, T> {
     {
         let view = self.entity();
         window.defer(self, move |window, cx| {
-            view.read(cx).focus_handle(cx).focus(window)
+            view.read(cx).focus_handle(cx).focus(window, cx)
         })
     }
 }

crates/gpui/src/app/test_context.rs πŸ”—

@@ -1045,7 +1045,7 @@ impl VisualContext for VisualTestContext {
     fn focus<V: crate::Focusable>(&mut self, view: &Entity<V>) -> Self::Result<()> {
         self.window
             .update(&mut self.cx, |_, window, cx| {
-                view.read(cx).focus_handle(cx).focus(window)
+                view.read(cx).focus_handle(cx).focus(window, cx)
             })
             .unwrap()
     }

crates/gpui/src/elements/div.rs πŸ”—

@@ -654,7 +654,7 @@ pub trait InteractiveElement: Sized {
     /// Set whether this element is a tab stop.
     ///
     /// When false, the element remains in tab-index order but cannot be reached via keyboard navigation.
-    /// Useful for container elements: focus the container, then call `window.focus_next()` to focus
+    /// Useful for container elements: focus the container, then call `window.focus_next(cx)` to focus
     /// the first tab stop inside it while having the container element itself be unreachable via the keyboard.
     /// Should only be used with `tab_index`.
     fn tab_stop(mut self, tab_stop: bool) -> Self {
@@ -2096,12 +2096,12 @@ impl Interactivity {
         // This behavior can be suppressed by using `cx.prevent_default()`.
         if let Some(focus_handle) = self.tracked_focus_handle.clone() {
             let hitbox = hitbox.clone();
-            window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _| {
+            window.on_mouse_event(move |_: &MouseDownEvent, phase, window, cx| {
                 if phase == DispatchPhase::Bubble
                     && hitbox.is_hovered(window)
                     && !window.default_prevented()
                 {
-                    window.focus(&focus_handle);
+                    window.focus(&focus_handle, cx);
                     // If there is a parent that is also focusable, prevent it
                     // from transferring focus because we already did so.
                     window.prevent_default();

crates/gpui/src/elements/surface.rs πŸ”—

@@ -29,6 +29,7 @@ pub struct Surface {
 }
 
 /// Create a new surface element.
+#[cfg(target_os = "macos")]
 pub fn surface(source: impl Into<SurfaceSource>) -> Surface {
     Surface {
         source: source.into(),

crates/gpui/src/elements/text.rs πŸ”—

@@ -6,6 +6,7 @@ use crate::{
     register_tooltip_mouse_handlers, set_tooltip_on_window,
 };
 use anyhow::Context as _;
+use itertools::Itertools;
 use smallvec::SmallVec;
 use std::{
     borrow::Cow,
@@ -597,14 +598,14 @@ impl TextLayout {
             .unwrap()
             .lines
             .iter()
-            .map(|s| s.text.to_string())
-            .collect::<Vec<_>>()
+            .map(|s| &s.text)
             .join("\n")
     }
 
     /// The text for this layout (with soft-wraps as newlines)
     pub fn wrapped_text(&self) -> String {
-        let mut lines = Vec::new();
+        let mut accumulator = String::new();
+
         for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
             let mut seen = 0;
             for boundary in wrapped.layout.wrap_boundaries.iter() {
@@ -612,13 +613,16 @@ impl TextLayout {
                     [boundary.glyph_ix]
                     .index;
 
-                lines.push(wrapped.text[seen..index].to_string());
+                accumulator.push_str(&wrapped.text[seen..index]);
+                accumulator.push('\n');
                 seen = index;
             }
-            lines.push(wrapped.text[seen..].to_string());
+            accumulator.push_str(&wrapped.text[seen..]);
+            accumulator.push('\n');
         }
-
-        lines.join("\n")
+        // Remove trailing newline
+        accumulator.pop();
+        accumulator
     }
 }
 

crates/gpui/src/elements/uniform_list.rs πŸ”—

@@ -712,8 +712,8 @@ mod test {
     #[gpui::test]
     fn test_scroll_strategy_nearest(cx: &mut TestAppContext) {
         use crate::{
-            Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, actions, div,
-            prelude::*, px, uniform_list,
+            Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, div, prelude::*,
+            px, uniform_list,
         };
         use std::ops::Range;
 
@@ -788,7 +788,7 @@ mod test {
 
         let (view, cx) = cx.add_window_view(|window, cx| {
             let focus_handle = cx.focus_handle();
-            window.focus(&focus_handle);
+            window.focus(&focus_handle, cx);
             TestView {
                 scroll_handle: UniformListScrollHandle::new(),
                 index: 0,

crates/gpui/src/interactive.rs πŸ”—

@@ -705,8 +705,8 @@ mod test {
         });
 
         window
-            .update(cx, |test_view, window, _cx| {
-                window.focus(&test_view.focus_handle)
+            .update(cx, |test_view, window, cx| {
+                window.focus(&test_view.focus_handle, cx)
             })
             .unwrap();
 

crates/gpui/src/key_dispatch.rs πŸ”—

@@ -462,6 +462,17 @@ impl DispatchTree {
         (bindings, partial, context_stack)
     }
 
+    /// Find the bindings that can follow the current input sequence.
+    pub fn possible_next_bindings_for_input(
+        &self,
+        input: &[Keystroke],
+        context_stack: &[KeyContext],
+    ) -> Vec<KeyBinding> {
+        self.keymap
+            .borrow()
+            .possible_next_bindings_for_input(input, context_stack)
+    }
+
     /// dispatch_key processes the keystroke
     /// input should be set to the value of `pending` from the previous call to dispatch_key.
     /// This returns three instructions to the input handler:
@@ -610,8 +621,8 @@ impl DispatchTree {
 #[cfg(test)]
 mod tests {
     use crate::{
-        self as gpui, DispatchResult, Element, ElementId, GlobalElementId, InspectorElementId,
-        Keystroke, LayoutId, Style,
+        self as gpui, AppContext, DispatchResult, Element, ElementId, GlobalElementId,
+        InspectorElementId, Keystroke, LayoutId, Style,
     };
     use core::panic;
     use smallvec::SmallVec;
@@ -619,8 +630,8 @@ mod tests {
 
     use crate::{
         Action, ActionRegistry, App, Bounds, Context, DispatchTree, FocusHandle, InputHandler,
-        IntoElement, KeyBinding, KeyContext, Keymap, Pixels, Point, Render, TestAppContext,
-        UTF16Selection, Window,
+        IntoElement, KeyBinding, KeyContext, Keymap, Pixels, Point, Render, Subscription,
+        TestAppContext, UTF16Selection, Window,
     };
 
     #[derive(PartialEq, Eq)]
@@ -723,6 +734,213 @@ mod tests {
         assert!(!result.pending_has_binding);
     }
 
+    #[crate::test]
+    fn test_pending_input_observers_notified_on_focus_change(cx: &mut TestAppContext) {
+        #[derive(Clone)]
+        struct CustomElement {
+            focus_handle: FocusHandle,
+            text: Rc<RefCell<String>>,
+        }
+
+        impl CustomElement {
+            fn new(cx: &mut Context<Self>) -> Self {
+                Self {
+                    focus_handle: cx.focus_handle(),
+                    text: Rc::default(),
+                }
+            }
+        }
+
+        impl Element for CustomElement {
+            type RequestLayoutState = ();
+
+            type PrepaintState = ();
+
+            fn id(&self) -> Option<ElementId> {
+                Some("custom".into())
+            }
+
+            fn source_location(&self) -> Option<&'static panic::Location<'static>> {
+                None
+            }
+
+            fn request_layout(
+                &mut self,
+                _: Option<&GlobalElementId>,
+                _: Option<&InspectorElementId>,
+                window: &mut Window,
+                cx: &mut App,
+            ) -> (LayoutId, Self::RequestLayoutState) {
+                (window.request_layout(Style::default(), [], cx), ())
+            }
+
+            fn prepaint(
+                &mut self,
+                _: Option<&GlobalElementId>,
+                _: Option<&InspectorElementId>,
+                _: Bounds<Pixels>,
+                _: &mut Self::RequestLayoutState,
+                window: &mut Window,
+                cx: &mut App,
+            ) -> Self::PrepaintState {
+                window.set_focus_handle(&self.focus_handle, cx);
+            }
+
+            fn paint(
+                &mut self,
+                _: Option<&GlobalElementId>,
+                _: Option<&InspectorElementId>,
+                _: Bounds<Pixels>,
+                _: &mut Self::RequestLayoutState,
+                _: &mut Self::PrepaintState,
+                window: &mut Window,
+                cx: &mut App,
+            ) {
+                let mut key_context = KeyContext::default();
+                key_context.add("Terminal");
+                window.set_key_context(key_context);
+                window.handle_input(&self.focus_handle, self.clone(), cx);
+                window.on_action(std::any::TypeId::of::<TestAction>(), |_, _, _, _| {});
+            }
+        }
+
+        impl IntoElement for CustomElement {
+            type Element = Self;
+
+            fn into_element(self) -> Self::Element {
+                self
+            }
+        }
+
+        impl InputHandler for CustomElement {
+            fn selected_text_range(
+                &mut self,
+                _: bool,
+                _: &mut Window,
+                _: &mut App,
+            ) -> Option<UTF16Selection> {
+                None
+            }
+
+            fn marked_text_range(&mut self, _: &mut Window, _: &mut App) -> Option<Range<usize>> {
+                None
+            }
+
+            fn text_for_range(
+                &mut self,
+                _: Range<usize>,
+                _: &mut Option<Range<usize>>,
+                _: &mut Window,
+                _: &mut App,
+            ) -> Option<String> {
+                None
+            }
+
+            fn replace_text_in_range(
+                &mut self,
+                replacement_range: Option<Range<usize>>,
+                text: &str,
+                _: &mut Window,
+                _: &mut App,
+            ) {
+                if replacement_range.is_some() {
+                    unimplemented!()
+                }
+                self.text.borrow_mut().push_str(text)
+            }
+
+            fn replace_and_mark_text_in_range(
+                &mut self,
+                replacement_range: Option<Range<usize>>,
+                new_text: &str,
+                _: Option<Range<usize>>,
+                _: &mut Window,
+                _: &mut App,
+            ) {
+                if replacement_range.is_some() {
+                    unimplemented!()
+                }
+                self.text.borrow_mut().push_str(new_text)
+            }
+
+            fn unmark_text(&mut self, _: &mut Window, _: &mut App) {}
+
+            fn bounds_for_range(
+                &mut self,
+                _: Range<usize>,
+                _: &mut Window,
+                _: &mut App,
+            ) -> Option<Bounds<Pixels>> {
+                None
+            }
+
+            fn character_index_for_point(
+                &mut self,
+                _: Point<Pixels>,
+                _: &mut Window,
+                _: &mut App,
+            ) -> Option<usize> {
+                None
+            }
+        }
+
+        impl Render for CustomElement {
+            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+                self.clone()
+            }
+        }
+
+        cx.update(|cx| {
+            cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]);
+            cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]);
+        });
+
+        let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx));
+        let focus_handle = test.update(cx, |test, _| test.focus_handle.clone());
+
+        let pending_input_changed_count = Rc::new(RefCell::new(0usize));
+        let pending_input_changed_count_for_observer = pending_input_changed_count.clone();
+
+        struct PendingInputObserver {
+            _subscription: Subscription,
+        }
+
+        let _observer = cx.update(|window, cx| {
+            cx.new(|cx| PendingInputObserver {
+                _subscription: cx.observe_pending_input(window, move |_, _, _| {
+                    *pending_input_changed_count_for_observer.borrow_mut() += 1;
+                }),
+            })
+        });
+
+        cx.update(|window, cx| {
+            window.focus(&focus_handle, cx);
+            window.activate_window();
+        });
+
+        cx.simulate_keystrokes("ctrl-b");
+
+        let count_after_pending = Rc::new(RefCell::new(0usize));
+        let count_after_pending_for_assertion = count_after_pending.clone();
+
+        cx.update(|window, cx| {
+            assert!(window.has_pending_keystrokes());
+            *count_after_pending.borrow_mut() = *pending_input_changed_count.borrow();
+            assert!(*count_after_pending.borrow() > 0);
+
+            window.focus(&cx.focus_handle(), cx);
+
+            assert!(!window.has_pending_keystrokes());
+        });
+
+        // Focus-triggered pending-input notifications are deferred to the end of the current
+        // effect cycle, so the observer callback should run after the focus update completes.
+        cx.update(|_, _| {
+            let count_after_focus_change = *pending_input_changed_count.borrow();
+            assert!(count_after_focus_change > *count_after_pending_for_assertion.borrow());
+        });
+    }
+
     #[crate::test]
     fn test_input_handler_pending(cx: &mut TestAppContext) {
         #[derive(Clone)]
@@ -876,8 +1094,9 @@ mod tests {
             cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]);
         });
         let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx));
+        let focus_handle = test.update(cx, |test, _| test.focus_handle.clone());
         cx.update(|window, cx| {
-            window.focus(&test.read(cx).focus_handle);
+            window.focus(&focus_handle, cx);
             window.activate_window();
         });
         cx.simulate_keystrokes("ctrl-b [");

crates/gpui/src/keymap.rs πŸ”—

@@ -215,6 +215,41 @@ impl Keymap {
             Some(contexts.len())
         }
     }
+
+    /// Find the bindings that can follow the current input sequence.
+    pub fn possible_next_bindings_for_input(
+        &self,
+        input: &[Keystroke],
+        context_stack: &[KeyContext],
+    ) -> Vec<KeyBinding> {
+        let mut bindings = self
+            .bindings()
+            .enumerate()
+            .rev()
+            .filter_map(|(ix, binding)| {
+                let depth = self.binding_enabled(binding, context_stack)?;
+                let pending = binding.match_keystrokes(input);
+                match pending {
+                    None => None,
+                    Some(is_pending) => {
+                        if !is_pending || is_no_action(&*binding.action) {
+                            return None;
+                        }
+                        Some((depth, BindingIndex(ix), binding))
+                    }
+                }
+            })
+            .collect::<Vec<_>>();
+
+        bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| {
+            depth_b.cmp(depth_a).then(ix_b.cmp(ix_a))
+        });
+
+        bindings
+            .into_iter()
+            .map(|(_, _, binding)| binding.clone())
+            .collect::<Vec<_>>()
+    }
 }
 
 #[cfg(test)]

crates/gpui/src/platform.rs πŸ”—

@@ -1348,6 +1348,10 @@ pub enum WindowKind {
     /// docks, notifications or wallpapers.
     #[cfg(all(target_os = "linux", feature = "wayland"))]
     LayerShell(layer_shell::LayerShellOptions),
+
+    /// A window that appears on top of its parent window and blocks interaction with it
+    /// until the modal window is closed
+    Dialog,
 }
 
 /// The appearance of the window, as defined by the operating system.

crates/gpui/src/platform/linux/wayland/client.rs πŸ”—

@@ -36,12 +36,6 @@ use wayland_client::{
         wl_shm_pool, wl_surface,
     },
 };
-use wayland_protocols::wp::cursor_shape::v1::client::{
-    wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
-};
-use wayland_protocols::wp::fractional_scale::v1::client::{
-    wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
-};
 use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{
     self, ZwpPrimarySelectionOfferV1,
 };
@@ -61,6 +55,14 @@ use wayland_protocols::xdg::decoration::zv1::client::{
     zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
 };
 use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
+use wayland_protocols::{
+    wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1},
+    xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1},
+};
+use wayland_protocols::{
+    wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1},
+    xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
+};
 use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
 use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
 use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
@@ -122,6 +124,7 @@ pub struct Globals {
     pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
     pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
     pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
+    pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
     pub executor: ForegroundExecutor,
 }
 
@@ -132,6 +135,7 @@ impl Globals {
         qh: QueueHandle<WaylandClientStatePtr>,
         seat: wl_seat::WlSeat,
     ) -> Self {
+        let dialog_v = XdgWmDialogV1::interface().version;
         Globals {
             activation: globals.bind(&qh, 1..=1, ()).ok(),
             compositor: globals
@@ -160,6 +164,7 @@ impl Globals {
             layer_shell: globals.bind(&qh, 1..=5, ()).ok(),
             blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
             text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
+            dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(),
             executor,
             qh,
         }
@@ -729,10 +734,7 @@ impl LinuxClient for WaylandClient {
     ) -> anyhow::Result<Box<dyn PlatformWindow>> {
         let mut state = self.0.borrow_mut();
 
-        let parent = state
-            .keyboard_focused_window
-            .as_ref()
-            .and_then(|w| w.toplevel());
+        let parent = state.keyboard_focused_window.clone();
 
         let (window, surface_id) = WaylandWindow::new(
             handle,
@@ -751,7 +753,12 @@ impl LinuxClient for WaylandClient {
     fn set_cursor_style(&self, style: CursorStyle) {
         let mut state = self.0.borrow_mut();
 
-        let need_update = state.cursor_style != Some(style);
+        let need_update = state.cursor_style != Some(style)
+            && (state.mouse_focused_window.is_none()
+                || state
+                    .mouse_focused_window
+                    .as_ref()
+                    .is_some_and(|w| !w.is_blocked()));
 
         if need_update {
             let serial = state.serial_tracker.get(SerialKind::MouseEnter);
@@ -1011,7 +1018,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
     }
 }
 
-fn get_window(
+pub(crate) fn get_window(
     mut state: &mut RefMut<WaylandClientState>,
     surface_id: &ObjectId,
 ) -> Option<WaylandWindowStatePtr> {
@@ -1654,6 +1661,30 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
                 state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
 
                 if let Some(window) = state.mouse_focused_window.clone() {
+                    if window.is_blocked() {
+                        let default_style = CursorStyle::Arrow;
+                        if state.cursor_style != Some(default_style) {
+                            let serial = state.serial_tracker.get(SerialKind::MouseEnter);
+                            state.cursor_style = Some(default_style);
+
+                            if let Some(cursor_shape_device) = &state.cursor_shape_device {
+                                cursor_shape_device.set_shape(serial, default_style.to_shape());
+                            } else {
+                                // cursor-shape-v1 isn't supported, set the cursor using a surface.
+                                let wl_pointer = state
+                                    .wl_pointer
+                                    .clone()
+                                    .expect("window is focused by pointer");
+                                let scale = window.primary_output_scale();
+                                state.cursor.set_icon(
+                                    &wl_pointer,
+                                    serial,
+                                    default_style.to_icon_names(),
+                                    scale,
+                                );
+                            }
+                        }
+                    }
                     if state
                         .keyboard_focused_window
                         .as_ref()
@@ -2225,3 +2256,27 @@ impl Dispatch<zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1, ()>
         }
     }
 }
+
+impl Dispatch<XdgWmDialogV1, ()> for WaylandClientStatePtr {
+    fn event(
+        _: &mut Self,
+        _: &XdgWmDialogV1,
+        _: <XdgWmDialogV1 as Proxy>::Event,
+        _: &(),
+        _: &Connection,
+        _: &QueueHandle<Self>,
+    ) {
+    }
+}
+
+impl Dispatch<XdgDialogV1, ()> for WaylandClientStatePtr {
+    fn event(
+        _state: &mut Self,
+        _proxy: &XdgDialogV1,
+        _event: <XdgDialogV1 as Proxy>::Event,
+        _data: &(),
+        _conn: &Connection,
+        _qhandle: &QueueHandle<Self>,
+    ) {
+    }
+}

crates/gpui/src/platform/linux/wayland/window.rs πŸ”—

@@ -7,7 +7,7 @@ use std::{
 };
 
 use blade_graphics as gpu;
-use collections::HashMap;
+use collections::{FxHashSet, HashMap};
 use futures::channel::oneshot::Receiver;
 
 use raw_window_handle as rwh;
@@ -20,7 +20,7 @@ use wayland_protocols::xdg::shell::client::xdg_surface;
 use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
 use wayland_protocols::{
     wp::fractional_scale::v1::client::wp_fractional_scale_v1,
-    xdg::shell::client::xdg_toplevel::XdgToplevel,
+    xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1,
 };
 use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
 use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1;
@@ -29,7 +29,7 @@ use crate::{
     AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
     PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
     ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
-    WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams,
+    WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, get_window,
     layer_shell::LayerShellNotSupportedError, px, size,
 };
 use crate::{
@@ -87,6 +87,8 @@ struct InProgressConfigure {
 pub struct WaylandWindowState {
     surface_state: WaylandSurfaceState,
     acknowledged_first_configure: bool,
+    parent: Option<WaylandWindowStatePtr>,
+    children: FxHashSet<ObjectId>,
     pub surface: wl_surface::WlSurface,
     app_id: Option<String>,
     appearance: WindowAppearance,
@@ -126,7 +128,7 @@ impl WaylandSurfaceState {
         surface: &wl_surface::WlSurface,
         globals: &Globals,
         params: &WindowParams,
-        parent: Option<XdgToplevel>,
+        parent: Option<WaylandWindowStatePtr>,
     ) -> anyhow::Result<Self> {
         // For layer_shell windows, create a layer surface instead of an xdg surface
         if let WindowKind::LayerShell(options) = &params.kind {
@@ -178,10 +180,28 @@ impl WaylandSurfaceState {
             .get_xdg_surface(&surface, &globals.qh, surface.id());
 
         let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
-        if params.kind == WindowKind::Floating {
-            toplevel.set_parent(parent.as_ref());
+        let xdg_parent = parent.as_ref().and_then(|w| w.toplevel());
+
+        if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
+            toplevel.set_parent(xdg_parent.as_ref());
         }
 
+        let dialog = if params.kind == WindowKind::Dialog {
+            let dialog = globals.dialog.as_ref().map(|dialog| {
+                let xdg_dialog = dialog.get_xdg_dialog(&toplevel, &globals.qh, ());
+                xdg_dialog.set_modal();
+                xdg_dialog
+            });
+
+            if let Some(parent) = parent.as_ref() {
+                parent.add_child(surface.id());
+            }
+
+            dialog
+        } else {
+            None
+        };
+
         if let Some(size) = params.window_min_size {
             toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
         }
@@ -198,6 +218,7 @@ impl WaylandSurfaceState {
             xdg_surface,
             toplevel,
             decoration,
+            dialog,
         }))
     }
 }
@@ -206,6 +227,7 @@ pub struct WaylandXdgSurfaceState {
     xdg_surface: xdg_surface::XdgSurface,
     toplevel: xdg_toplevel::XdgToplevel,
     decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
+    dialog: Option<XdgDialogV1>,
 }
 
 pub struct WaylandLayerSurfaceState {
@@ -258,7 +280,13 @@ impl WaylandSurfaceState {
                 xdg_surface,
                 toplevel,
                 decoration: _decoration,
+                dialog,
             }) => {
+                // drop the dialog before toplevel so compositor can explicitly unapply it's effects
+                if let Some(dialog) = dialog {
+                    dialog.destroy();
+                }
+
                 // The role object (toplevel) must always be destroyed before the xdg_surface.
                 // See https://wayland.app/protocols/xdg-shell#xdg_surface:request:destroy
                 toplevel.destroy();
@@ -288,6 +316,7 @@ impl WaylandWindowState {
         globals: Globals,
         gpu_context: &BladeContext,
         options: WindowParams,
+        parent: Option<WaylandWindowStatePtr>,
     ) -> anyhow::Result<Self> {
         let renderer = {
             let raw_window = RawWindow {
@@ -319,6 +348,8 @@ impl WaylandWindowState {
         Ok(Self {
             surface_state,
             acknowledged_first_configure: false,
+            parent,
+            children: FxHashSet::default(),
             surface,
             app_id: None,
             blur: None,
@@ -391,6 +422,10 @@ impl Drop for WaylandWindow {
     fn drop(&mut self) {
         let mut state = self.0.state.borrow_mut();
         let surface_id = state.surface.id();
+        if let Some(parent) = state.parent.as_ref() {
+            parent.state.borrow_mut().children.remove(&surface_id);
+        }
+
         let client = state.client.clone();
 
         state.renderer.destroy();
@@ -448,10 +483,10 @@ impl WaylandWindow {
         client: WaylandClientStatePtr,
         params: WindowParams,
         appearance: WindowAppearance,
-        parent: Option<XdgToplevel>,
+        parent: Option<WaylandWindowStatePtr>,
     ) -> anyhow::Result<(Self, ObjectId)> {
         let surface = globals.compositor.create_surface(&globals.qh, ());
-        let surface_state = WaylandSurfaceState::new(&surface, &globals, &params, parent)?;
+        let surface_state = WaylandSurfaceState::new(&surface, &globals, &params, parent.clone())?;
 
         if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
             fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
@@ -473,6 +508,7 @@ impl WaylandWindow {
                 globals,
                 gpu_context,
                 params,
+                parent,
             )?)),
             callbacks: Rc::new(RefCell::new(Callbacks::default())),
         });
@@ -501,6 +537,16 @@ impl WaylandWindowStatePtr {
         Rc::ptr_eq(&self.state, &other.state)
     }
 
+    pub fn add_child(&self, child: ObjectId) {
+        let mut state = self.state.borrow_mut();
+        state.children.insert(child);
+    }
+
+    pub fn is_blocked(&self) -> bool {
+        let state = self.state.borrow();
+        !state.children.is_empty()
+    }
+
     pub fn frame(&self) {
         let mut state = self.state.borrow_mut();
         state.surface.frame(&state.globals.qh, state.surface.id());
@@ -818,6 +864,9 @@ impl WaylandWindowStatePtr {
     }
 
     pub fn handle_ime(&self, ime: ImeInput) {
+        if self.is_blocked() {
+            return;
+        }
         let mut state = self.state.borrow_mut();
         if let Some(mut input_handler) = state.input_handler.take() {
             drop(state);
@@ -894,6 +943,21 @@ impl WaylandWindowStatePtr {
     }
 
     pub fn close(&self) {
+        let state = self.state.borrow();
+        let client = state.client.get_client();
+        #[allow(clippy::mutable_key_type)]
+        let children = state.children.clone();
+        drop(state);
+
+        for child in children {
+            let mut client_state = client.borrow_mut();
+            let window = get_window(&mut client_state, &child);
+            drop(client_state);
+
+            if let Some(child) = window {
+                child.close();
+            }
+        }
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(fun) = callbacks.close.take() {
             fun()
@@ -901,6 +965,9 @@ impl WaylandWindowStatePtr {
     }
 
     pub fn handle_input(&self, input: PlatformInput) {
+        if self.is_blocked() {
+            return;
+        }
         if let Some(ref mut fun) = self.callbacks.borrow_mut().input
             && !fun(input.clone()).propagate
         {
@@ -1025,13 +1092,26 @@ impl PlatformWindow for WaylandWindow {
     fn resize(&mut self, size: Size<Pixels>) {
         let state = self.borrow();
         let state_ptr = self.0.clone();
-        let dp_size = size.to_device_pixels(self.scale_factor());
+
+        // Keep window geometry consistent with configure handling. On Wayland, window geometry is
+        // surface-local: resizing should not attempt to translate the window; the compositor
+        // controls placement. We also account for client-side decoration insets and tiling.
+        let window_geometry = inset_by_tiling(
+            Bounds {
+                origin: Point::default(),
+                size,
+            },
+            state.inset(),
+            state.tiling,
+        )
+        .map(|v| v.0 as i32)
+        .map_size(|v| if v <= 0 { 1 } else { v });
 
         state.surface_state.set_geometry(
-            state.bounds.origin.x.0 as i32,
-            state.bounds.origin.y.0 as i32,
-            dp_size.width.0,
-            dp_size.height.0,
+            window_geometry.origin.x,
+            window_geometry.origin.y,
+            window_geometry.size.width,
+            window_geometry.size.height,
         );
 
         state

crates/gpui/src/platform/linux/x11/client.rs πŸ”—

@@ -222,7 +222,7 @@ pub struct X11ClientState {
 pub struct X11ClientStatePtr(pub Weak<RefCell<X11ClientState>>);
 
 impl X11ClientStatePtr {
-    fn get_client(&self) -> Option<X11Client> {
+    pub fn get_client(&self) -> Option<X11Client> {
         self.0.upgrade().map(X11Client)
     }
 
@@ -752,7 +752,7 @@ impl X11Client {
         }
     }
 
-    fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
+    pub(crate) fn get_window(&self, win: xproto::Window) -> Option<X11WindowStatePtr> {
         let state = self.0.borrow();
         state
             .windows
@@ -789,12 +789,12 @@ impl X11Client {
                 let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32();
                 let mut state = self.0.borrow_mut();
 
-                if atom == state.atoms.WM_DELETE_WINDOW {
+                if atom == state.atoms.WM_DELETE_WINDOW && window.should_close() {
                     // window "x" button clicked by user
-                    if window.should_close() {
-                        // Rest of the close logic is handled in drop_window()
-                        window.close();
-                    }
+                    // Rest of the close logic is handled in drop_window()
+                    drop(state);
+                    window.close();
+                    state = self.0.borrow_mut();
                 } else if atom == state.atoms._NET_WM_SYNC_REQUEST {
                     window.state.borrow_mut().last_sync_counter =
                         Some(x11rb::protocol::sync::Int64 {
@@ -1216,6 +1216,33 @@ impl X11Client {
             Event::XinputMotion(event) => {
                 let window = self.get_window(event.event)?;
                 let mut state = self.0.borrow_mut();
+                if window.is_blocked() {
+                    // We want to set the cursor to the default arrow
+                    // when the window is blocked
+                    let style = CursorStyle::Arrow;
+
+                    let current_style = state
+                        .cursor_styles
+                        .get(&window.x_window)
+                        .unwrap_or(&CursorStyle::Arrow);
+                    if *current_style != style
+                        && let Some(cursor) = state.get_cursor_icon(style)
+                    {
+                        state.cursor_styles.insert(window.x_window, style);
+                        check_reply(
+                            || "Failed to set cursor style",
+                            state.xcb_connection.change_window_attributes(
+                                window.x_window,
+                                &ChangeWindowAttributesAux {
+                                    cursor: Some(cursor),
+                                    ..Default::default()
+                                },
+                            ),
+                        )
+                        .log_err();
+                        state.xcb_connection.flush().log_err();
+                    };
+                }
                 let pressed_button = pressed_button_from_mask(event.button_mask[0]);
                 let position = point(
                     px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
@@ -1489,7 +1516,7 @@ impl LinuxClient for X11Client {
         let parent_window = state
             .keyboard_focused_window
             .and_then(|focused_window| state.windows.get(&focused_window))
-            .map(|window| window.window.x_window);
+            .map(|w| w.window.clone());
         let x_window = state
             .xcb_connection
             .generate_id()
@@ -1544,7 +1571,15 @@ impl LinuxClient for X11Client {
             .cursor_styles
             .get(&focused_window)
             .unwrap_or(&CursorStyle::Arrow);
-        if *current_style == style {
+
+        let window = state
+            .mouse_focused_window
+            .and_then(|w| state.windows.get(&w));
+
+        let should_change = *current_style != style
+            && (window.is_none() || window.is_some_and(|w| !w.is_blocked()));
+
+        if !should_change {
             return;
         }
 

crates/gpui/src/platform/linux/x11/window.rs πŸ”—

@@ -11,6 +11,7 @@ use crate::{
 };
 
 use blade_graphics as gpu;
+use collections::FxHashSet;
 use raw_window_handle as rwh;
 use util::{ResultExt, maybe};
 use x11rb::{
@@ -74,6 +75,7 @@ x11rb::atom_manager! {
         _NET_WM_WINDOW_TYPE,
         _NET_WM_WINDOW_TYPE_NOTIFICATION,
         _NET_WM_WINDOW_TYPE_DIALOG,
+        _NET_WM_STATE_MODAL,
         _NET_WM_SYNC,
         _NET_SUPPORTED,
         _MOTIF_WM_HINTS,
@@ -249,6 +251,8 @@ pub struct Callbacks {
 
 pub struct X11WindowState {
     pub destroyed: bool,
+    parent: Option<X11WindowStatePtr>,
+    children: FxHashSet<xproto::Window>,
     client: X11ClientStatePtr,
     executor: ForegroundExecutor,
     atoms: XcbAtoms,
@@ -394,7 +398,7 @@ impl X11WindowState {
         atoms: &XcbAtoms,
         scale_factor: f32,
         appearance: WindowAppearance,
-        parent_window: Option<xproto::Window>,
+        parent_window: Option<X11WindowStatePtr>,
     ) -> anyhow::Result<Self> {
         let x_screen_index = params
             .display_id
@@ -546,8 +550,8 @@ impl X11WindowState {
                 )?;
             }
 
-            if params.kind == WindowKind::Floating {
-                if let Some(parent_window) = parent_window {
+            if params.kind == WindowKind::Floating || params.kind == WindowKind::Dialog {
+                if let Some(parent_window) = parent_window.as_ref().map(|w| w.x_window) {
                     // WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set
                     // a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to
                     // place the floating window in relation to the main window.
@@ -563,11 +567,23 @@ impl X11WindowState {
                         ),
                     )?;
                 }
+            }
+
+            let parent = if params.kind == WindowKind::Dialog
+                && let Some(parent) = parent_window
+            {
+                parent.add_child(x_window);
+
+                Some(parent)
+            } else {
+                None
+            };
 
+            if params.kind == WindowKind::Dialog {
                 // _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window
                 // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
                 check_reply(
-                    || "X11 ChangeProperty32 setting window type for floating window failed.",
+                    || "X11 ChangeProperty32 setting window type for dialog window failed.",
                     xcb.change_property32(
                         xproto::PropMode::REPLACE,
                         x_window,
@@ -576,6 +592,20 @@ impl X11WindowState {
                         &[atoms._NET_WM_WINDOW_TYPE_DIALOG],
                     ),
                 )?;
+
+                // We set the modal state for dialog windows, so that the window manager
+                // can handle it appropriately (e.g., prevent interaction with the parent window
+                // while the dialog is open).
+                check_reply(
+                    || "X11 ChangeProperty32 setting modal state for dialog window failed.",
+                    xcb.change_property32(
+                        xproto::PropMode::REPLACE,
+                        x_window,
+                        atoms._NET_WM_STATE,
+                        xproto::AtomEnum::ATOM,
+                        &[atoms._NET_WM_STATE_MODAL],
+                    ),
+                )?;
             }
 
             check_reply(
@@ -667,6 +697,8 @@ impl X11WindowState {
             let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?);
 
             Ok(Self {
+                parent,
+                children: FxHashSet::default(),
                 client,
                 executor,
                 display,
@@ -720,6 +752,11 @@ pub(crate) struct X11Window(pub X11WindowStatePtr);
 impl Drop for X11Window {
     fn drop(&mut self) {
         let mut state = self.0.state.borrow_mut();
+
+        if let Some(parent) = state.parent.as_ref() {
+            parent.state.borrow_mut().children.remove(&self.0.x_window);
+        }
+
         state.renderer.destroy();
 
         let destroy_x_window = maybe!({
@@ -734,8 +771,6 @@ impl Drop for X11Window {
         .log_err();
 
         if destroy_x_window.is_some() {
-            // Mark window as destroyed so that we can filter out when X11 events
-            // for it still come in.
             state.destroyed = true;
 
             let this_ptr = self.0.clone();
@@ -773,7 +808,7 @@ impl X11Window {
         atoms: &XcbAtoms,
         scale_factor: f32,
         appearance: WindowAppearance,
-        parent_window: Option<xproto::Window>,
+        parent_window: Option<X11WindowStatePtr>,
     ) -> anyhow::Result<Self> {
         let ptr = X11WindowStatePtr {
             state: Rc::new(RefCell::new(X11WindowState::new(
@@ -979,7 +1014,31 @@ impl X11WindowStatePtr {
         Ok(())
     }
 
+    pub fn add_child(&self, child: xproto::Window) {
+        let mut state = self.state.borrow_mut();
+        state.children.insert(child);
+    }
+
+    pub fn is_blocked(&self) -> bool {
+        let state = self.state.borrow();
+        !state.children.is_empty()
+    }
+
     pub fn close(&self) {
+        let state = self.state.borrow();
+        let client = state.client.clone();
+        #[allow(clippy::mutable_key_type)]
+        let children = state.children.clone();
+        drop(state);
+
+        if let Some(client) = client.get_client() {
+            for child in children {
+                if let Some(child_window) = client.get_window(child) {
+                    child_window.close();
+                }
+            }
+        }
+
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(fun) = callbacks.close.take() {
             fun()
@@ -994,6 +1053,9 @@ impl X11WindowStatePtr {
     }
 
     pub fn handle_input(&self, input: PlatformInput) {
+        if self.is_blocked() {
+            return;
+        }
         if let Some(ref mut fun) = self.callbacks.borrow_mut().input
             && !fun(input.clone()).propagate
         {
@@ -1016,6 +1078,9 @@ impl X11WindowStatePtr {
     }
 
     pub fn handle_ime_commit(&self, text: String) {
+        if self.is_blocked() {
+            return;
+        }
         let mut state = self.state.borrow_mut();
         if let Some(mut input_handler) = state.input_handler.take() {
             drop(state);
@@ -1026,6 +1091,9 @@ impl X11WindowStatePtr {
     }
 
     pub fn handle_ime_preedit(&self, text: String) {
+        if self.is_blocked() {
+            return;
+        }
         let mut state = self.state.borrow_mut();
         if let Some(mut input_handler) = state.input_handler.take() {
             drop(state);
@@ -1036,6 +1104,9 @@ impl X11WindowStatePtr {
     }
 
     pub fn handle_ime_unmark(&self) {
+        if self.is_blocked() {
+            return;
+        }
         let mut state = self.state.borrow_mut();
         if let Some(mut input_handler) = state.input_handler.take() {
             drop(state);
@@ -1046,6 +1117,9 @@ impl X11WindowStatePtr {
     }
 
     pub fn handle_ime_delete(&self) {
+        if self.is_blocked() {
+            return;
+        }
         let mut state = self.state.borrow_mut();
         if let Some(mut input_handler) = state.input_handler.take() {
             drop(state);

crates/gpui/src/platform/mac/metal_atlas.rs πŸ”—

@@ -15,9 +15,6 @@ pub(crate) struct MetalAtlas(Mutex<MetalAtlasState>);
 impl MetalAtlas {
     pub(crate) fn new(device: Device) -> Self {
         MetalAtlas(Mutex::new(MetalAtlasState {
-            // Shared memory can be used only if CPU and GPU share the same memory space.
-            // https://developer.apple.com/documentation/metal/setting-resource-storage-modes
-            unified_memory: device.has_unified_memory(),
             device: AssertSend(device),
             monochrome_textures: Default::default(),
             polychrome_textures: Default::default(),
@@ -32,7 +29,6 @@ impl MetalAtlas {
 
 struct MetalAtlasState {
     device: AssertSend<Device>,
-    unified_memory: bool,
     monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
     polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
     tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
@@ -150,11 +146,6 @@ impl MetalAtlasState {
         }
         texture_descriptor.set_pixel_format(pixel_format);
         texture_descriptor.set_usage(usage);
-        texture_descriptor.set_storage_mode(if self.unified_memory {
-            metal::MTLStorageMode::Shared
-        } else {
-            metal::MTLStorageMode::Managed
-        });
         let metal_texture = self.device.new_texture(&texture_descriptor);
 
         let texture_list = match kind {

crates/gpui/src/platform/mac/metal_renderer.rs πŸ”—

@@ -46,9 +46,9 @@ pub unsafe fn new_renderer(
     _native_window: *mut c_void,
     _native_view: *mut c_void,
     _bounds: crate::Size<f32>,
-    _transparent: bool,
+    transparent: bool,
 ) -> Renderer {
-    MetalRenderer::new(context)
+    MetalRenderer::new(context, transparent)
 }
 
 pub(crate) struct InstanceBufferPool {
@@ -76,22 +76,12 @@ impl InstanceBufferPool {
         self.buffers.clear();
     }
 
-    pub(crate) fn acquire(
-        &mut self,
-        device: &metal::Device,
-        unified_memory: bool,
-    ) -> InstanceBuffer {
+    pub(crate) fn acquire(&mut self, device: &metal::Device) -> InstanceBuffer {
         let buffer = self.buffers.pop().unwrap_or_else(|| {
-            let options = if unified_memory {
-                MTLResourceOptions::StorageModeShared
-                    // Buffers are write only which can benefit from the combined cache
-                    // https://developer.apple.com/documentation/metal/mtlresourceoptions/cpucachemodewritecombined
-                    | MTLResourceOptions::CPUCacheModeWriteCombined
-            } else {
-                MTLResourceOptions::StorageModeManaged
-            };
-
-            device.new_buffer(self.buffer_size as u64, options)
+            device.new_buffer(
+                self.buffer_size as u64,
+                MTLResourceOptions::StorageModeManaged,
+            )
         });
         InstanceBuffer {
             metal_buffer: buffer,
@@ -109,7 +99,6 @@ impl InstanceBufferPool {
 pub(crate) struct MetalRenderer {
     device: metal::Device,
     layer: metal::MetalLayer,
-    unified_memory: bool,
     presents_with_transaction: bool,
     command_queue: CommandQueue,
     paths_rasterization_pipeline_state: metal::RenderPipelineState,
@@ -139,7 +128,7 @@ pub struct PathRasterizationVertex {
 }
 
 impl MetalRenderer {
-    pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
+    pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>, transparent: bool) -> Self {
         // Prefer low‐power integrated GPUs on Intel Mac. On Apple
         // Silicon, there is only ever one GPU, so this is equivalent to
         // `metal::Device::system_default()`.
@@ -163,8 +152,13 @@ impl MetalRenderer {
         let layer = metal::MetalLayer::new();
         layer.set_device(&device);
         layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
-        layer.set_opaque(false);
+        // Support direct-to-display rendering if the window is not transparent
+        // https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos
+        layer.set_opaque(!transparent);
         layer.set_maximum_drawable_count(3);
+        // We already present at display sync with the display link
+        // This allows to use direct-to-display even in window mode
+        layer.set_display_sync_enabled(false);
         unsafe {
             let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
             let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
@@ -190,10 +184,6 @@ impl MetalRenderer {
             output
         }
 
-        // Shared memory can be used only if CPU and GPU share the same memory space.
-        // https://developer.apple.com/documentation/metal/setting-resource-storage-modes
-        let unified_memory = device.has_unified_memory();
-
         let unit_vertices = [
             to_float2_bits(point(0., 0.)),
             to_float2_bits(point(1., 0.)),
@@ -205,12 +195,7 @@ impl MetalRenderer {
         let unit_vertices = device.new_buffer_with_data(
             unit_vertices.as_ptr() as *const c_void,
             mem::size_of_val(&unit_vertices) as u64,
-            if unified_memory {
-                MTLResourceOptions::StorageModeShared
-                    | MTLResourceOptions::CPUCacheModeWriteCombined
-            } else {
-                MTLResourceOptions::StorageModeManaged
-            },
+            MTLResourceOptions::StorageModeManaged,
         );
 
         let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state(
@@ -288,7 +273,6 @@ impl MetalRenderer {
             device,
             layer,
             presents_with_transaction: false,
-            unified_memory,
             command_queue,
             paths_rasterization_pipeline_state,
             path_sprites_pipeline_state,
@@ -358,23 +342,14 @@ impl MetalRenderer {
         texture_descriptor.set_width(size.width.0 as u64);
         texture_descriptor.set_height(size.height.0 as u64);
         texture_descriptor.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm);
-        texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
         texture_descriptor
             .set_usage(metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead);
         self.path_intermediate_texture = Some(self.device.new_texture(&texture_descriptor));
 
         if self.path_sample_count > 1 {
-            // https://developer.apple.com/documentation/metal/choosing-a-resource-storage-mode-for-apple-gpus
-            // Rendering MSAA textures are done in a single pass, so we can use memory-less storage on Apple Silicon
-            let storage_mode = if self.unified_memory {
-                metal::MTLStorageMode::Memoryless
-            } else {
-                metal::MTLStorageMode::Private
-            };
-
             let mut msaa_descriptor = texture_descriptor;
             msaa_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample);
-            msaa_descriptor.set_storage_mode(storage_mode);
+            msaa_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
             msaa_descriptor.set_sample_count(self.path_sample_count as _);
             self.path_intermediate_msaa_texture = Some(self.device.new_texture(&msaa_descriptor));
         } else {
@@ -382,8 +357,8 @@ impl MetalRenderer {
         }
     }
 
-    pub fn update_transparency(&self, _transparent: bool) {
-        // todo(mac)?
+    pub fn update_transparency(&self, transparent: bool) {
+        self.layer.set_opaque(!transparent);
     }
 
     pub fn destroy(&self) {
@@ -408,10 +383,7 @@ impl MetalRenderer {
         };
 
         loop {
-            let mut instance_buffer = self
-                .instance_buffer_pool
-                .lock()
-                .acquire(&self.device, self.unified_memory);
+            let mut instance_buffer = self.instance_buffer_pool.lock().acquire(&self.device);
 
             let command_buffer =
                 self.draw_primitives(scene, &mut instance_buffer, drawable, viewport_size);
@@ -583,14 +555,10 @@ impl MetalRenderer {
 
         command_encoder.end_encoding();
 
-        if !self.unified_memory {
-            // Sync the instance buffer to the GPU
-            instance_buffer.metal_buffer.did_modify_range(NSRange {
-                location: 0,
-                length: instance_offset as NSUInteger,
-            });
-        }
-
+        instance_buffer.metal_buffer.did_modify_range(NSRange {
+            location: 0,
+            length: instance_offset as NSUInteger,
+        });
         Ok(command_buffer.to_owned())
     }
 

crates/gpui/src/platform/mac/open_type.rs πŸ”—

@@ -52,6 +52,11 @@ pub fn apply_features_and_fallbacks(
             &kCFTypeDictionaryKeyCallBacks,
             &kCFTypeDictionaryValueCallBacks,
         );
+
+        for value in &values {
+            CFRelease(*value as _);
+        }
+
         let new_descriptor = CTFontDescriptorCreateWithAttributes(attrs);
         CFRelease(attrs as _);
         let new_descriptor = CTFontDescriptor::wrap_under_create_rule(new_descriptor);

crates/gpui/src/platform/mac/screen_capture.rs πŸ”—

@@ -110,13 +110,21 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
             let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64];
             let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate];
 
+            // Stream contains filter, configuration, and delegate internally so we release them here
+            // to prevent a memory leak when steam is dropped
+            let _: () = msg_send![filter, release];
+            let _: () = msg_send![configuration, release];
+            let _: () = msg_send![delegate, release];
+
             let (mut tx, rx) = oneshot::channel();
 
             let mut error: id = nil;
             let _: () = msg_send![stream, addStreamOutput:output type:SCStreamOutputTypeScreen sampleHandlerQueue:0 error:&mut error as *mut id];
             if error != nil {
                 let message: id = msg_send![error, localizedDescription];
-                tx.send(Err(anyhow!("failed to add stream  output {message:?}")))
+                let _: () = msg_send![stream, release];
+                let _: () = msg_send![output, release];
+                tx.send(Err(anyhow!("failed to add stream output {message:?}")))
                     .ok();
                 return rx;
             }
@@ -132,8 +140,10 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
                         };
                         Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>)
                     } else {
+                        let _: () = msg_send![stream, release];
+                        let _: () = msg_send![output, release];
                         let message: id = msg_send![error, localizedDescription];
-                        Err(anyhow!("failed to stop screen capture stream {message:?}"))
+                        Err(anyhow!("failed to start screen capture stream {message:?}"))
                     };
                     if let Some(tx) = tx.borrow_mut().take() {
                         tx.send(result).ok();

crates/gpui/src/platform/mac/text_system.rs πŸ”—

@@ -8,6 +8,7 @@ use anyhow::anyhow;
 use cocoa::appkit::CGFloat;
 use collections::HashMap;
 use core_foundation::{
+    array::{CFArray, CFArrayRef},
     attributed_string::CFMutableAttributedString,
     base::{CFRange, TCFType},
     number::CFNumber,
@@ -21,8 +22,10 @@ use core_graphics::{
 };
 use core_text::{
     font::CTFont,
+    font_collection::CTFontCollectionRef,
     font_descriptor::{
-        kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait, kCTFontWidthTrait,
+        CTFontDescriptor, kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait,
+        kCTFontWidthTrait,
     },
     line::CTLine,
     string_attributes::kCTFontAttributeName,
@@ -97,7 +100,26 @@ impl PlatformTextSystem for MacTextSystem {
     fn all_font_names(&self) -> Vec<String> {
         let mut names = Vec::new();
         let collection = core_text::font_collection::create_for_all_families();
-        let Some(descriptors) = collection.get_descriptors() else {
+        // NOTE: We intentionally avoid using `collection.get_descriptors()` here because
+        // it has a memory leak bug in core-text v21.0.0. The upstream code uses
+        // `wrap_under_get_rule` but `CTFontCollectionCreateMatchingFontDescriptors`
+        // follows the Create Rule (caller owns the result), so it should use
+        // `wrap_under_create_rule`. We call the function directly with correct memory management.
+        unsafe extern "C" {
+            fn CTFontCollectionCreateMatchingFontDescriptors(
+                collection: CTFontCollectionRef,
+            ) -> CFArrayRef;
+        }
+        let descriptors: Option<CFArray<CTFontDescriptor>> = unsafe {
+            let array_ref =
+                CTFontCollectionCreateMatchingFontDescriptors(collection.as_concrete_TypeRef());
+            if array_ref.is_null() {
+                None
+            } else {
+                Some(CFArray::wrap_under_create_rule(array_ref))
+            }
+        };
+        let Some(descriptors) = descriptors else {
             return names;
         };
         for descriptor in descriptors.into_iter() {

crates/gpui/src/platform/mac/window.rs πŸ”—

@@ -62,9 +62,12 @@ static mut BLURRED_VIEW_CLASS: *const Class = ptr::null();
 #[allow(non_upper_case_globals)]
 const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
     NSWindowStyleMask::from_bits_retain(1 << 7);
+// WindowLevel const value ref: https://docs.rs/core-graphics2/0.4.1/src/core_graphics2/window_level.rs.html
 #[allow(non_upper_case_globals)]
 const NSNormalWindowLevel: NSInteger = 0;
 #[allow(non_upper_case_globals)]
+const NSFloatingWindowLevel: NSInteger = 3;
+#[allow(non_upper_case_globals)]
 const NSPopUpWindowLevel: NSInteger = 101;
 #[allow(non_upper_case_globals)]
 const NSTrackingMouseEnteredAndExited: NSUInteger = 0x01;
@@ -423,6 +426,8 @@ struct MacWindowState {
     select_previous_tab_callback: Option<Box<dyn FnMut()>>,
     toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
     activated_least_once: bool,
+    // The parent window if this window is a sheet (Dialog kind)
+    sheet_parent: Option<id>,
 }
 
 impl MacWindowState {
@@ -622,11 +627,16 @@ impl MacWindow {
             }
 
             let native_window: id = match kind {
-                WindowKind::Normal | WindowKind::Floating => msg_send![WINDOW_CLASS, alloc],
+                WindowKind::Normal => {
+                    msg_send![WINDOW_CLASS, alloc]
+                }
                 WindowKind::PopUp => {
                     style_mask |= NSWindowStyleMaskNonactivatingPanel;
                     msg_send![PANEL_CLASS, alloc]
                 }
+                WindowKind::Floating | WindowKind::Dialog => {
+                    msg_send![PANEL_CLASS, alloc]
+                }
             };
 
             let display = display_id
@@ -729,6 +739,7 @@ impl MacWindow {
                 select_previous_tab_callback: None,
                 toggle_tab_bar_callback: None,
                 activated_least_once: false,
+                sheet_parent: None,
             })));
 
             (*native_window).set_ivar(
@@ -779,9 +790,18 @@ impl MacWindow {
             content_view.addSubview_(native_view.autorelease());
             native_window.makeFirstResponder_(native_view);
 
+            let app: id = NSApplication::sharedApplication(nil);
+            let main_window: id = msg_send![app, mainWindow];
+            let mut sheet_parent = None;
+
             match kind {
                 WindowKind::Normal | WindowKind::Floating => {
-                    native_window.setLevel_(NSNormalWindowLevel);
+                    if kind == WindowKind::Floating {
+                        // Let the window float keep above normal windows.
+                        native_window.setLevel_(NSFloatingWindowLevel);
+                    } else {
+                        native_window.setLevel_(NSNormalWindowLevel);
+                    }
                     native_window.setAcceptsMouseMovedEvents_(YES);
 
                     if let Some(tabbing_identifier) = tabbing_identifier {
@@ -816,10 +836,23 @@ impl MacWindow {
                         NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
                     );
                 }
+                WindowKind::Dialog => {
+                    if !main_window.is_null() {
+                        let parent = {
+                            let active_sheet: id = msg_send![main_window, attachedSheet];
+                            if active_sheet.is_null() {
+                                main_window
+                            } else {
+                                active_sheet
+                            }
+                        };
+                        let _: () =
+                            msg_send![parent, beginSheet: native_window completionHandler: nil];
+                        sheet_parent = Some(parent);
+                    }
+                }
             }
 
-            let app = NSApplication::sharedApplication(nil);
-            let main_window: id = msg_send![app, mainWindow];
             if allows_automatic_window_tabbing
                 && !main_window.is_null()
                 && main_window != native_window
@@ -861,7 +894,11 @@ impl MacWindow {
             // the window position might be incorrect if the main screen (the screen that contains the window that has focus)
             //  is different from the primary screen.
             NSWindow::setFrameTopLeftPoint_(native_window, window_rect.origin);
-            window.0.lock().move_traffic_light();
+            {
+                let mut window_state = window.0.lock();
+                window_state.move_traffic_light();
+                window_state.sheet_parent = sheet_parent;
+            }
 
             pool.drain();
 
@@ -938,6 +975,7 @@ impl Drop for MacWindow {
         let mut this = self.0.lock();
         this.renderer.destroy();
         let window = this.native_window;
+        let sheet_parent = this.sheet_parent.take();
         this.display_link.take();
         unsafe {
             this.native_window.setDelegate_(nil);
@@ -946,6 +984,9 @@ impl Drop for MacWindow {
         this.executor
             .spawn(async move {
                 unsafe {
+                    if let Some(parent) = sheet_parent {
+                        let _: () = msg_send![parent, endSheet: window];
+                    }
                     window.close();
                     window.autorelease();
                 }
@@ -1190,6 +1231,7 @@ impl PlatformWindow for MacWindow {
             let (done_tx, done_rx) = oneshot::channel();
             let done_tx = Cell::new(Some(done_tx));
             let block = ConcreteBlock::new(move |answer: NSInteger| {
+                let _: () = msg_send![alert, release];
                 if let Some(done_tx) = done_tx.take() {
                     let _ = done_tx.send(answer.try_into().unwrap());
                 }

crates/gpui/src/platform/windows/events.rs πŸ”—

@@ -40,6 +40,11 @@ impl WindowsWindowInner {
         lparam: LPARAM,
     ) -> LRESULT {
         let handled = match msg {
+            // eagerly activate the window, so calls to `active_window` will work correctly
+            WM_MOUSEACTIVATE => {
+                unsafe { SetActiveWindow(handle).log_err() };
+                None
+            }
             WM_ACTIVATE => self.handle_activate_msg(wparam),
             WM_CREATE => self.handle_create_msg(handle),
             WM_MOVE => self.handle_move_msg(handle, lparam),
@@ -265,6 +270,14 @@ impl WindowsWindowInner {
 
     fn handle_destroy_msg(&self, handle: HWND) -> Option<isize> {
         let callback = { self.state.callbacks.close.take() };
+        // Re-enable parent window if this was a modal dialog
+        if let Some(parent_hwnd) = self.parent_hwnd {
+            unsafe {
+                let _ = EnableWindow(parent_hwnd, true);
+                let _ = SetForegroundWindow(parent_hwnd);
+            }
+        }
+
         if let Some(callback) = callback {
             callback();
         }

crates/gpui/src/platform/windows/platform.rs πŸ”—

@@ -659,7 +659,7 @@ impl Platform for WindowsPlatform {
             if let Err(err) = result {
                 // ERROR_NOT_FOUND means the credential doesn't exist.
                 // Return Ok(None) to match macOS and Linux behavior.
-                if err.code().0 == ERROR_NOT_FOUND.0 as i32 {
+                if err.code() == ERROR_NOT_FOUND.to_hresult() {
                     return Ok(None);
                 }
                 return Err(err.into());

crates/gpui/src/platform/windows/window.rs πŸ”—

@@ -83,6 +83,7 @@ pub(crate) struct WindowsWindowInner {
     pub(crate) validation_number: usize,
     pub(crate) main_receiver: flume::Receiver<RunnableVariant>,
     pub(crate) platform_window_handle: HWND,
+    pub(crate) parent_hwnd: Option<HWND>,
 }
 
 impl WindowsWindowState {
@@ -241,6 +242,7 @@ impl WindowsWindowInner {
             main_receiver: context.main_receiver.clone(),
             platform_window_handle: context.platform_window_handle,
             system_settings: WindowsSystemSettings::new(context.display),
+            parent_hwnd: context.parent_hwnd,
         }))
     }
 
@@ -368,6 +370,7 @@ struct WindowCreateContext {
     disable_direct_composition: bool,
     directx_devices: DirectXDevices,
     invalidate_devices: Arc<AtomicBool>,
+    parent_hwnd: Option<HWND>,
 }
 
 impl WindowsWindow {
@@ -390,6 +393,20 @@ impl WindowsWindow {
             invalidate_devices,
         } = creation_info;
         register_window_class(icon);
+        let parent_hwnd = if params.kind == WindowKind::Dialog {
+            let parent_window = unsafe { GetActiveWindow() };
+            if parent_window.is_invalid() {
+                None
+            } else {
+                // Disable the parent window to make this dialog modal
+                unsafe {
+                    EnableWindow(parent_window, false).as_bool();
+                };
+                Some(parent_window)
+            }
+        } else {
+            None
+        };
         let hide_title_bar = params
             .titlebar
             .as_ref()
@@ -416,8 +433,14 @@ impl WindowsWindow {
             if params.is_minimizable {
                 dwstyle |= WS_MINIMIZEBOX;
             }
+            let dwexstyle = if params.kind == WindowKind::Dialog {
+                dwstyle |= WS_POPUP | WS_CAPTION;
+                WS_EX_DLGMODALFRAME
+            } else {
+                WS_EX_APPWINDOW
+            };
 
-            (WS_EX_APPWINDOW, dwstyle)
+            (dwexstyle, dwstyle)
         };
         if !disable_direct_composition {
             dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
@@ -449,6 +472,7 @@ impl WindowsWindow {
             disable_direct_composition,
             directx_devices,
             invalidate_devices,
+            parent_hwnd,
         };
         let creation_result = unsafe {
             CreateWindowExW(
@@ -460,7 +484,7 @@ impl WindowsWindow {
                 CW_USEDEFAULT,
                 CW_USEDEFAULT,
                 CW_USEDEFAULT,
-                None,
+                parent_hwnd,
                 None,
                 Some(hinstance.into()),
                 Some(&context as *const _ as *const _),

crates/gpui/src/test.rs πŸ”—

@@ -69,7 +69,10 @@ pub fn run_test(
                         std::mem::forget(error);
                     } else {
                         if is_multiple_runs {
-                            eprintln!("failing seed: {}", seed);
+                            eprintln!("failing seed: {seed}");
+                            eprintln!(
+                                "You can rerun from this seed by setting the environmental variable SEED to {seed}"
+                            );
                         }
                         if let Some(on_fail_fn) = on_fail_fn {
                             on_fail_fn()

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

@@ -128,22 +128,21 @@ impl LineWrapper {
         })
     }
 
-    /// Truncate a line of text to the given width with this wrapper's font and font size.
-    pub fn truncate_line<'a>(
+    /// Determines if a line should be truncated based on its width.
+    pub fn should_truncate_line(
         &mut self,
-        line: SharedString,
+        line: &str,
         truncate_width: Pixels,
         truncation_suffix: &str,
-        runs: &'a [TextRun],
-    ) -> (SharedString, Cow<'a, [TextRun]>) {
+    ) -> Option<usize> {
         let mut width = px(0.);
-        let mut suffix_width = truncation_suffix
+        let suffix_width = truncation_suffix
             .chars()
             .map(|c| self.width_for_char(c))
             .fold(px(0.0), |a, x| a + x);
-        let mut char_indices = line.char_indices();
         let mut truncate_ix = 0;
-        for (ix, c) in char_indices {
+
+        for (ix, c) in line.char_indices() {
             if width + suffix_width < truncate_width {
                 truncate_ix = ix;
             }
@@ -152,16 +151,32 @@ impl LineWrapper {
             width += char_width;
 
             if width.floor() > truncate_width {
-                let result =
-                    SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
-                let mut runs = runs.to_vec();
-                update_runs_after_truncation(&result, truncation_suffix, &mut runs);
-
-                return (result, Cow::Owned(runs));
+                return Some(truncate_ix);
             }
         }
 
-        (line, Cow::Borrowed(runs))
+        None
+    }
+
+    /// Truncate a line of text to the given width with this wrapper's font and font size.
+    pub fn truncate_line<'a>(
+        &mut self,
+        line: SharedString,
+        truncate_width: Pixels,
+        truncation_suffix: &str,
+        runs: &'a [TextRun],
+    ) -> (SharedString, Cow<'a, [TextRun]>) {
+        if let Some(truncate_ix) =
+            self.should_truncate_line(&line, truncate_width, truncation_suffix)
+        {
+            let result =
+                SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
+            let mut runs = runs.to_vec();
+            update_runs_after_truncation(&result, truncation_suffix, &mut runs);
+            (result, Cow::Owned(runs))
+        } else {
+            (line, Cow::Borrowed(runs))
+        }
     }
 
     /// Any character in this list should be treated as a word character,
@@ -182,6 +197,11 @@ impl LineWrapper {
         // Cyrillic for Russian, Ukrainian, etc.
         // https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
         matches!(c, '\u{0400}'..='\u{04FF}') ||
+
+        // Vietnamese (https://vietunicode.sourceforge.net/charset/)
+        matches!(c, '\u{1E00}'..='\u{1EFF}') || // Latin Extended Additional
+        matches!(c, '\u{0300}'..='\u{036F}') || // Combining Diacritical Marks
+
         // 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.
@@ -618,7 +638,12 @@ mod tests {
         #[track_caller]
         fn assert_word(word: &str) {
             for c in word.chars() {
-                assert!(LineWrapper::is_word_char(c), "assertion failed for '{}'", c);
+                assert!(
+                    LineWrapper::is_word_char(c),
+                    "assertion failed for '{}' (unicode 0x{:x})",
+                    c,
+                    c as u32
+                );
             }
         }
 
@@ -661,6 +686,8 @@ mod tests {
         assert_word("Ζ€ΖΖ‚ΖƒΖ„Ζ…Ζ†Ζ‡ΖˆΖ‰ΖŠΖ‹ΖŒΖΖŽΖ");
         // Cyrillic
         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");
 
         // non-word characters
         assert_not_word("δ½ ε₯½");

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

@@ -345,8 +345,8 @@ impl FocusHandle {
     }
 
     /// Moves the focus to the element associated with this handle.
-    pub fn focus(&self, window: &mut Window) {
-        window.focus(self)
+    pub fn focus(&self, window: &mut Window, cx: &mut App) {
+        window.focus(self, cx)
     }
 
     /// Obtains whether the element associated with this handle is currently focused.
@@ -1436,13 +1436,25 @@ impl Window {
     }
 
     /// Move focus to the element associated with the given [`FocusHandle`].
-    pub fn focus(&mut self, handle: &FocusHandle) {
+    pub fn focus(&mut self, handle: &FocusHandle, cx: &mut App) {
         if !self.focus_enabled || self.focus == Some(handle.id) {
             return;
         }
 
         self.focus = Some(handle.id);
         self.clear_pending_keystrokes();
+
+        // Avoid re-entrant entity updates by deferring observer notifications to the end of the
+        // current effect cycle, and only for this window.
+        let window_handle = self.handle;
+        cx.defer(move |cx| {
+            window_handle
+                .update(cx, |_, window, cx| {
+                    window.pending_input_changed(cx);
+                })
+                .ok();
+        });
+
         self.refresh();
     }
 
@@ -1463,24 +1475,24 @@ impl Window {
     }
 
     /// Move focus to next tab stop.
-    pub fn focus_next(&mut self) {
+    pub fn focus_next(&mut self, cx: &mut App) {
         if !self.focus_enabled {
             return;
         }
 
         if let Some(handle) = self.rendered_frame.tab_stops.next(self.focus.as_ref()) {
-            self.focus(&handle)
+            self.focus(&handle, cx)
         }
     }
 
     /// Move focus to previous tab stop.
-    pub fn focus_prev(&mut self) {
+    pub fn focus_prev(&mut self, cx: &mut App) {
         if !self.focus_enabled {
             return;
         }
 
         if let Some(handle) = self.rendered_frame.tab_stops.prev(self.focus.as_ref()) {
-            self.focus(&handle)
+            self.focus(&handle, cx)
         }
     }
 
@@ -1961,7 +1973,7 @@ impl Window {
     }
 
     /// Determine whether the given action is available along the dispatch path to the currently focused element.
-    pub fn is_action_available(&self, action: &dyn Action, cx: &mut App) -> bool {
+    pub fn is_action_available(&self, action: &dyn Action, cx: &App) -> bool {
         let node_id =
             self.focus_node_id_in_rendered_frame(self.focused(cx).map(|handle| handle.id));
         self.rendered_frame
@@ -1969,6 +1981,14 @@ impl Window {
             .is_action_available(action, node_id)
     }
 
+    /// Determine whether the given action is available along the dispatch path to the given focus_handle.
+    pub fn is_action_available_in(&self, action: &dyn Action, focus_handle: &FocusHandle) -> bool {
+        let node_id = self.focus_node_id_in_rendered_frame(Some(focus_handle.id));
+        self.rendered_frame
+            .dispatch_tree
+            .is_action_available(action, node_id)
+    }
+
     /// The position of the mouse relative to the window.
     pub fn mouse_position(&self) -> Point<Pixels> {
         self.mouse_position
@@ -4012,7 +4032,7 @@ impl Window {
         self.dispatch_keystroke_observers(event, None, context_stack, cx);
     }
 
-    fn pending_input_changed(&mut self, cx: &mut App) {
+    pub(crate) fn pending_input_changed(&mut self, cx: &mut App) {
         self.pending_input_observers
             .clone()
             .retain(&(), |callback| callback(self, cx));
@@ -4430,6 +4450,13 @@ impl Window {
         dispatch_tree.highest_precedence_binding_for_action(action, &context_stack)
     }
 
+    /// Find the bindings that can follow the current input sequence for the current context stack.
+    pub fn possible_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
+        self.rendered_frame
+            .dispatch_tree
+            .possible_next_bindings_for_input(input, &self.context_stack())
+    }
+
     fn context_stack_for_focus_handle(
         &self,
         focus_handle: &FocusHandle,
@@ -4939,7 +4966,7 @@ impl<V: 'static> From<WindowHandle<V>> for AnyWindowHandle {
 }
 
 /// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
-#[derive(Copy, Clone, PartialEq, Eq, Hash)]
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
 pub struct AnyWindowHandle {
     pub(crate) id: WindowId,
     state_type: TypeId,

crates/gpui/src/window/prompts.rs πŸ”—

@@ -44,10 +44,10 @@ impl PromptHandle {
             if let Some(sender) = sender.take() {
                 sender.send(e.0).ok();
                 window_handle
-                    .update(cx, |_, window, _cx| {
+                    .update(cx, |_, window, cx| {
                         window.prompt.take();
                         if let Some(previous_focus) = &previous_focus {
-                            window.focus(previous_focus);
+                            window.focus(previous_focus, cx);
                         }
                     })
                     .ok();
@@ -55,7 +55,7 @@ impl PromptHandle {
         })
         .detach();
 
-        window.focus(&view.focus_handle(cx));
+        window.focus(&view.focus_handle(cx), cx);
 
         RenderablePromptHandle {
             view: Box::new(view),

crates/gpui_macros/src/derive_visual_context.rs πŸ”—

@@ -62,7 +62,7 @@ pub fn derive_visual_context(input: TokenStream) -> TokenStream {
                 V: gpui::Focusable,
             {
                 let focus_handle = gpui::Focusable::focus_handle(entity, self.#app_variable);
-                self.#window_variable.focus(&focus_handle)
+                self.#window_variable.focus(&focus_handle, self.#app_variable)
             }
         }
     };

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

@@ -911,7 +911,7 @@ impl KeymapEditor {
             .focus_handle(cx)
             .contains_focused(window, cx)
         {
-            window.focus(&self.filter_editor.focus_handle(cx));
+            window.focus(&self.filter_editor.focus_handle(cx), cx);
         } else {
             self.filter_editor.update(cx, |editor, cx| {
                 editor.select_all(&Default::default(), window, cx);
@@ -948,7 +948,7 @@ impl KeymapEditor {
             if let Some(scroll_strategy) = scroll {
                 self.scroll_to_item(index, scroll_strategy, cx);
             }
-            window.focus(&self.focus_handle);
+            window.focus(&self.focus_handle, cx);
             cx.notify();
         }
     }
@@ -998,7 +998,7 @@ impl KeymapEditor {
             });
 
             let context_menu_handle = context_menu.focus_handle(cx);
-            window.defer(cx, move |window, _cx| window.focus(&context_menu_handle));
+            window.defer(cx, move |window, cx| window.focus(&context_menu_handle, cx));
             let subscription = cx.subscribe_in(
                 &context_menu,
                 window,
@@ -1014,7 +1014,7 @@ impl KeymapEditor {
 
     fn dismiss_context_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         self.context_menu.take();
-        window.focus(&self.focus_handle);
+        window.focus(&self.focus_handle, cx);
         cx.notify();
     }
 
@@ -1230,7 +1230,7 @@ impl KeymapEditor {
                         window,
                         cx,
                     );
-                    window.focus(&modal.focus_handle(cx));
+                    window.focus(&modal.focus_handle(cx), cx);
                     modal
                 });
             })
@@ -1338,7 +1338,7 @@ impl KeymapEditor {
                     editor.stop_recording(&StopRecording, window, cx);
                     editor.clear_keystrokes(&ClearKeystrokes, window, cx);
                 });
-                window.focus(&self.filter_editor.focus_handle(cx));
+                window.focus(&self.filter_editor.focus_handle(cx), cx);
             }
         }
     }
@@ -2698,32 +2698,32 @@ impl KeybindingEditorModalFocusState {
             .map(|i| i as i32)
     }
 
-    fn focus_index(&self, mut index: i32, window: &mut Window) {
+    fn focus_index(&self, mut index: i32, window: &mut Window, cx: &mut App) {
         if index < 0 {
             index = self.handles.len() as i32 - 1;
         }
         if index >= self.handles.len() as i32 {
             index = 0;
         }
-        window.focus(&self.handles[index as usize]);
+        window.focus(&self.handles[index as usize], cx);
     }
 
-    fn focus_next(&self, window: &mut Window, cx: &App) {
+    fn focus_next(&self, window: &mut Window, cx: &mut App) {
         let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
             index + 1
         } else {
             0
         };
-        self.focus_index(index_to_focus, window);
+        self.focus_index(index_to_focus, window, cx);
     }
 
-    fn focus_previous(&self, window: &mut Window, cx: &App) {
+    fn focus_previous(&self, window: &mut Window, cx: &mut App) {
         let index_to_focus = if let Some(index) = self.focused_index(window, cx) {
             index - 1
         } else {
             self.handles.len() as i32 - 1
         };
-        self.focus_index(index_to_focus, window);
+        self.focus_index(index_to_focus, window, cx);
     }
 }
 
@@ -2757,7 +2757,7 @@ impl ActionArgumentsEditor {
     ) -> Self {
         let focus_handle = cx.focus_handle();
         cx.on_focus_in(&focus_handle, window, |this, window, cx| {
-            this.editor.focus_handle(cx).focus(window);
+            this.editor.focus_handle(cx).focus(window, cx);
         })
         .detach();
         let editor = cx.new(|cx| {
@@ -2810,7 +2810,7 @@ impl ActionArgumentsEditor {
 
                 this.update_in(cx, |this, window, cx| {
                     if this.editor.focus_handle(cx).is_focused(window) {
-                        editor.focus_handle(cx).focus(window);
+                        editor.focus_handle(cx).focus(window, cx);
                     }
                     this.editor = editor;
                     this.backup_temp_dir = backup_temp_dir;

crates/keymap_editor/src/ui_components/keystroke_input.rs πŸ”—

@@ -388,7 +388,7 @@ impl KeystrokeInput {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        window.focus(&self.inner_focus_handle);
+        window.focus(&self.inner_focus_handle, cx);
         self.clear_keystrokes(&ClearKeystrokes, window, cx);
         self.previous_modifiers = window.modifiers();
         #[cfg(test)]
@@ -407,7 +407,7 @@ impl KeystrokeInput {
         if !self.is_recording(window) {
             return;
         }
-        window.focus(&self.outer_focus_handle);
+        window.focus(&self.outer_focus_handle, cx);
         if let Some(close_keystrokes_start) = self.close_keystrokes_start.take()
             && close_keystrokes_start < self.keystrokes.len()
         {

crates/language/Cargo.toml πŸ”—

@@ -32,6 +32,7 @@ async-trait.workspace = true
 clock.workspace = true
 collections.workspace = true
 ec4rs.workspace = true
+encoding_rs.workspace = true
 fs.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
@@ -48,6 +49,7 @@ rand = { workspace = true, optional = true }
 regex.workspace = true
 rpc.workspace = true
 schemars.workspace = true
+semver.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/language/src/buffer.rs πŸ”—

@@ -8,8 +8,8 @@ use crate::{
     outline::OutlineItem,
     row_chunk::RowChunks,
     syntax_map::{
-        SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatch,
-        SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint,
+        MAX_BYTES_TO_QUERY, SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures,
+        SyntaxMapMatch, SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint,
     },
     task_context::RunnableRange,
     text_diff::text_diff,
@@ -25,6 +25,7 @@ use anyhow::{Context as _, Result};
 use clock::Lamport;
 pub use clock::ReplicaId;
 use collections::{HashMap, HashSet};
+use encoding_rs::Encoding;
 use fs::MTime;
 use futures::channel::oneshot;
 use gpui::{
@@ -131,6 +132,8 @@ pub struct Buffer {
     change_bits: Vec<rc::Weak<Cell<bool>>>,
     _subscriptions: Vec<gpui::Subscription>,
     tree_sitter_data: Arc<TreeSitterData>,
+    encoding: &'static Encoding,
+    has_bom: bool,
 }
 
 #[derive(Debug)]
@@ -1100,6 +1103,8 @@ impl Buffer {
             has_conflict: false,
             change_bits: Default::default(),
             _subscriptions: Vec::new(),
+            encoding: encoding_rs::UTF_8,
+            has_bom: false,
         }
     }
 
@@ -1383,6 +1388,26 @@ impl Buffer {
         self.saved_mtime
     }
 
+    /// Returns the character encoding of the buffer's file.
+    pub fn encoding(&self) -> &'static Encoding {
+        self.encoding
+    }
+
+    /// Sets the character encoding of the buffer.
+    pub fn set_encoding(&mut self, encoding: &'static Encoding) {
+        self.encoding = encoding;
+    }
+
+    /// Returns whether the buffer has a Byte Order Mark.
+    pub fn has_bom(&self) -> bool {
+        self.has_bom
+    }
+
+    /// Sets whether the buffer has a Byte Order Mark.
+    pub fn set_has_bom(&mut self, has_bom: bool) {
+        self.has_bom = has_bom;
+    }
+
     /// Assign a language to the buffer.
     pub fn set_language_async(&mut self, language: Option<Arc<Language>>, cx: &mut Context<Self>) {
         self.set_language_(language, cfg!(any(test, feature = "test-support")), cx);
@@ -1776,9 +1801,7 @@ impl Buffer {
         self.syntax_map.lock().did_parse(syntax_snapshot);
         self.request_autoindent(cx);
         self.parse_status.0.send(ParseStatus::Idle).unwrap();
-        if self.text.version() != *self.tree_sitter_data.version() {
-            self.invalidate_tree_sitter_data(self.text.snapshot());
-        }
+        self.invalidate_tree_sitter_data(self.text.snapshot());
         cx.emit(BufferEvent::Reparsed);
         cx.notify();
     }
@@ -3216,15 +3239,22 @@ impl BufferSnapshot {
         struct StartPosition {
             start: Point,
             suffix: SharedString,
+            language: Arc<Language>,
         }
 
         // Find the suggested indentation ranges based on the syntax tree.
         let start = Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0);
         let end = Point::new(row_range.end, 0);
         let range = (start..end).to_offset(&self.text);
-        let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
-            Some(&grammar.indents_config.as_ref()?.query)
-        });
+        let mut matches = self.syntax.matches_with_options(
+            range.clone(),
+            &self.text,
+            TreeSitterOptions {
+                max_bytes_to_query: Some(MAX_BYTES_TO_QUERY),
+                max_start_depth: None,
+            },
+            |grammar| Some(&grammar.indents_config.as_ref()?.query),
+        );
         let indent_configs = matches
             .grammars()
             .iter()
@@ -3253,6 +3283,7 @@ impl BufferSnapshot {
                     start_positions.push(StartPosition {
                         start: Point::from_ts_point(capture.node.start_position()),
                         suffix: suffix.clone(),
+                        language: mat.language.clone(),
                     });
                 }
             }
@@ -3303,8 +3334,7 @@ impl BufferSnapshot {
             // set its end to the outdent position
             if let Some(range_to_truncate) = indent_ranges
                 .iter_mut()
-                .filter(|indent_range| indent_range.contains(&outdent_position))
-                .next_back()
+                .rfind(|indent_range| indent_range.contains(&outdent_position))
             {
                 range_to_truncate.end = outdent_position;
             }
@@ -3314,7 +3344,7 @@ impl BufferSnapshot {
 
         // Find the suggested indentation increases and decreased based on regexes.
         let mut regex_outdent_map = HashMap::default();
-        let mut last_seen_suffix: HashMap<String, Vec<Point>> = HashMap::default();
+        let mut last_seen_suffix: HashMap<String, Vec<StartPosition>> = HashMap::default();
         let mut start_positions_iter = start_positions.iter().peekable();
 
         let mut indent_change_rows = Vec::<(u32, Ordering)>::new();
@@ -3322,14 +3352,21 @@ impl BufferSnapshot {
             Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0)
                 ..Point::new(row_range.end, 0),
             |row, line| {
-                if config
+                let indent_len = self.indent_size_for_line(row).len;
+                let row_language = self.language_at(Point::new(row, indent_len)).cloned();
+                let row_language_config = row_language
+                    .as_ref()
+                    .map(|lang| lang.config())
+                    .unwrap_or(config);
+
+                if row_language_config
                     .decrease_indent_pattern
                     .as_ref()
                     .is_some_and(|regex| regex.is_match(line))
                 {
                     indent_change_rows.push((row, Ordering::Less));
                 }
-                if config
+                if row_language_config
                     .increase_indent_pattern
                     .as_ref()
                     .is_some_and(|regex| regex.is_match(line))
@@ -3338,16 +3375,16 @@ impl BufferSnapshot {
                 }
                 while let Some(pos) = start_positions_iter.peek() {
                     if pos.start.row < row {
-                        let pos = start_positions_iter.next().unwrap();
+                        let pos = start_positions_iter.next().unwrap().clone();
                         last_seen_suffix
                             .entry(pos.suffix.to_string())
                             .or_default()
-                            .push(pos.start);
+                            .push(pos);
                     } else {
                         break;
                     }
                 }
-                for rule in &config.decrease_indent_patterns {
+                for rule in &row_language_config.decrease_indent_patterns {
                     if rule.pattern.as_ref().is_some_and(|r| r.is_match(line)) {
                         let row_start_column = self.indent_size_for_line(row).len;
                         let basis_row = rule
@@ -3355,10 +3392,16 @@ impl BufferSnapshot {
                             .iter()
                             .filter_map(|valid_suffix| last_seen_suffix.get(valid_suffix))
                             .flatten()
-                            .filter(|start_point| start_point.column <= row_start_column)
-                            .max_by_key(|start_point| start_point.row);
-                        if let Some(outdent_to_row) = basis_row {
-                            regex_outdent_map.insert(row, outdent_to_row.row);
+                            .filter(|pos| {
+                                row_language
+                                    .as_ref()
+                                    .or(self.language.as_ref())
+                                    .is_some_and(|lang| Arc::ptr_eq(lang, &pos.language))
+                            })
+                            .filter(|pos| pos.start.column <= row_start_column)
+                            .max_by_key(|pos| pos.start.row);
+                        if let Some(outdent_to) = basis_row {
+                            regex_outdent_map.insert(row, outdent_to.start.row);
                         }
                         break;
                     }
@@ -4336,11 +4379,15 @@ impl BufferSnapshot {
             let mut opens = Vec::new();
             let mut color_pairs = Vec::new();
 
-            let mut matches = self
-                .syntax
-                .matches(chunk_range.clone(), &self.text, |grammar| {
-                    grammar.brackets_config.as_ref().map(|c| &c.query)
-                });
+            let mut matches = self.syntax.matches_with_options(
+                chunk_range.clone(),
+                &self.text,
+                TreeSitterOptions {
+                    max_bytes_to_query: Some(MAX_BYTES_TO_QUERY),
+                    max_start_depth: None,
+                },
+                |grammar| grammar.brackets_config.as_ref().map(|c| &c.query),
+            );
             let configs = matches
                 .grammars()
                 .iter()

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

@@ -43,6 +43,7 @@ pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQue
 use parking_lot::Mutex;
 use regex::Regex;
 use schemars::{JsonSchema, SchemaGenerator, json_schema};
+use semver::Version;
 use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
 use serde_json::Value;
 use settings::WorktreeId;
@@ -329,6 +330,10 @@ impl CachedLspAdapter {
             .cloned()
             .unwrap_or_else(|| language_name.lsp_id())
     }
+
+    pub fn process_prompt_response(&self, context: &PromptResponseContext, cx: &mut AsyncApp) {
+        self.adapter.process_prompt_response(context, cx)
+    }
 }
 
 /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application
@@ -347,13 +352,24 @@ pub trait LspAdapterDelegate: Send + Sync {
     async fn npm_package_installed_version(
         &self,
         package_name: &str,
-    ) -> Result<Option<(PathBuf, String)>>;
+    ) -> Result<Option<(PathBuf, Version)>>;
     async fn which(&self, command: &OsStr) -> Option<PathBuf>;
     async fn shell_env(&self) -> HashMap<String, String>;
     async fn read_text_file(&self, path: &RelPath) -> Result<String>;
     async fn try_exec(&self, binary: LanguageServerBinary) -> Result<()>;
 }
 
+/// Context provided to LSP adapters when a user responds to a ShowMessageRequest prompt.
+/// This allows adapters to intercept preference selections (like "Always" or "Never")
+/// and potentially persist them to Zed's settings.
+#[derive(Debug, Clone)]
+pub struct PromptResponseContext {
+    /// The original message shown to the user
+    pub message: String,
+    /// The action (button) the user selected
+    pub selected_action: lsp::MessageActionItem,
+}
+
 #[async_trait(?Send)]
 pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller {
     fn name(&self) -> LanguageServerName;
@@ -510,6 +526,11 @@ pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller {
     fn is_extension(&self) -> bool {
         false
     }
+
+    /// Called when a user responds to a ShowMessageRequest from this language server.
+    /// This allows adapters to intercept preference selections (like "Always" or "Never")
+    /// for settings that should be persisted to Zed's settings file.
+    fn process_prompt_response(&self, _context: &PromptResponseContext, _cx: &mut AsyncApp) {}
 }
 
 pub trait LspInstaller {

crates/language/src/syntax_map.rs πŸ”—

@@ -21,6 +21,8 @@ use sum_tree::{Bias, Dimensions, SeekTarget, SumTree};
 use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint};
 use tree_sitter::{Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatches, Tree};
 
+pub const MAX_BYTES_TO_QUERY: usize = 16 * 1024;
+
 pub struct SyntaxMap {
     snapshot: SyntaxSnapshot,
     language_registry: Option<Arc<LanguageRegistry>>,
@@ -1096,12 +1098,15 @@ impl<'a> SyntaxMapCaptures<'a> {
 
 #[derive(Default)]
 pub struct TreeSitterOptions {
-    max_start_depth: Option<u32>,
+    pub max_start_depth: Option<u32>,
+    pub max_bytes_to_query: Option<usize>,
 }
+
 impl TreeSitterOptions {
     pub fn max_start_depth(max_start_depth: u32) -> Self {
         Self {
             max_start_depth: Some(max_start_depth),
+            max_bytes_to_query: None,
         }
     }
 }
@@ -1135,6 +1140,14 @@ impl<'a> SyntaxMapMatches<'a> {
             };
             cursor.set_max_start_depth(options.max_start_depth);
 
+            if let Some(max_bytes_to_query) = options.max_bytes_to_query {
+                let midpoint = (range.start + range.end) / 2;
+                let containing_range_start = midpoint.saturating_sub(max_bytes_to_query / 2);
+                let containing_range_end =
+                    containing_range_start.saturating_add(max_bytes_to_query);
+                cursor.set_containing_byte_range(containing_range_start..containing_range_end);
+            }
+
             cursor.set_byte_range(range.clone());
             let matches = cursor.matches(query, layer.node(), TextProvider(text));
             let grammar_index = result
@@ -1642,6 +1655,10 @@ impl<'a> SyntaxLayer<'a> {
 
         let mut query_cursor = QueryCursorHandle::new();
         query_cursor.set_byte_range(offset.saturating_sub(1)..offset.saturating_add(1));
+        query_cursor.set_containing_byte_range(
+            offset.saturating_sub(MAX_BYTES_TO_QUERY / 2)
+                ..offset.saturating_add(MAX_BYTES_TO_QUERY / 2),
+        );
 
         let mut smallest_match: Option<(u32, Range<usize>)> = None;
         let mut matches = query_cursor.matches(&config.query, self.node(), text);
@@ -1928,6 +1945,8 @@ impl Drop for QueryCursorHandle {
         let mut cursor = self.0.take().unwrap();
         cursor.set_byte_range(0..usize::MAX);
         cursor.set_point_range(Point::zero().to_ts_point()..Point::MAX.to_ts_point());
+        cursor.set_containing_byte_range(0..usize::MAX);
+        cursor.set_containing_point_range(Point::zero().to_ts_point()..Point::MAX.to_ts_point());
         QUERY_CURSORS.lock().push(cursor)
     }
 }

crates/language/src/text_diff.rs πŸ”—

@@ -48,7 +48,6 @@ pub fn text_diff(old_text: &str, new_text: &str) -> Vec<(Range<usize>, Arc<str>)
 ///
 /// Returns a tuple of (old_ranges, new_ranges) where each vector contains
 /// the byte ranges of changed words in the respective text.
-/// Whitespace-only changes are excluded from the results.
 pub fn word_diff_ranges(
     old_text: &str,
     new_text: &str,
@@ -62,23 +61,23 @@ pub fn word_diff_ranges(
     let mut new_ranges: Vec<Range<usize>> = Vec::new();
 
     diff_internal(&input, |old_byte_range, new_byte_range, _, _| {
-        for range in split_on_whitespace(old_text, &old_byte_range) {
+        if !old_byte_range.is_empty() {
             if let Some(last) = old_ranges.last_mut()
-                && last.end >= range.start
+                && last.end >= old_byte_range.start
             {
-                last.end = range.end;
+                last.end = old_byte_range.end;
             } else {
-                old_ranges.push(range);
+                old_ranges.push(old_byte_range);
             }
         }
 
-        for range in split_on_whitespace(new_text, &new_byte_range) {
+        if !new_byte_range.is_empty() {
             if let Some(last) = new_ranges.last_mut()
-                && last.end >= range.start
+                && last.end >= new_byte_range.start
             {
-                last.end = range.end;
+                last.end = new_byte_range.end;
             } else {
-                new_ranges.push(range);
+                new_ranges.push(new_byte_range);
             }
         }
     });
@@ -86,50 +85,6 @@ pub fn word_diff_ranges(
     (old_ranges, new_ranges)
 }
 
-fn split_on_whitespace(text: &str, range: &Range<usize>) -> Vec<Range<usize>> {
-    if range.is_empty() {
-        return Vec::new();
-    }
-
-    let slice = &text[range.clone()];
-    let mut ranges = Vec::new();
-    let mut offset = 0;
-
-    for line in slice.lines() {
-        let line_start = offset;
-        let line_end = line_start + line.len();
-        offset = line_end + 1;
-        let trimmed = line.trim();
-
-        if !trimmed.is_empty() {
-            let leading = line.len() - line.trim_start().len();
-            let trailing = line.len() - line.trim_end().len();
-            let trimmed_start = range.start + line_start + leading;
-            let trimmed_end = range.start + line_end - trailing;
-
-            let original_line_start = text[..range.start + line_start]
-                .rfind('\n')
-                .map(|i| i + 1)
-                .unwrap_or(0);
-            let original_line_end = text[range.start + line_start..]
-                .find('\n')
-                .map(|i| range.start + line_start + i)
-                .unwrap_or(text.len());
-            let original_line = &text[original_line_start..original_line_end];
-            let original_trimmed_start =
-                original_line_start + (original_line.len() - original_line.trim_start().len());
-            let original_trimmed_end =
-                original_line_end - (original_line.len() - original_line.trim_end().len());
-
-            if trimmed_start > original_trimmed_start || trimmed_end < original_trimmed_end {
-                ranges.push(trimmed_start..trimmed_end);
-            }
-        }
-    }
-
-    ranges
-}
-
 pub struct DiffOptions {
     pub language_scope: Option<LanguageScope>,
     pub max_word_diff_len: usize,

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

@@ -1,6 +1,6 @@
 use anthropic::{
-    ANTHROPIC_API_URL, AnthropicError, AnthropicModelMode, ContentDelta, Event, ResponseContent,
-    ToolResultContent, ToolResultPart, Usage,
+    ANTHROPIC_API_URL, AnthropicError, AnthropicModelMode, ContentDelta, CountTokensRequest, Event,
+    ResponseContent, ToolResultContent, ToolResultPart, Usage,
 };
 use anyhow::{Result, anyhow};
 use collections::{BTreeMap, HashMap};
@@ -219,68 +219,215 @@ pub struct AnthropicModel {
     request_limiter: RateLimiter,
 }
 
-pub fn count_anthropic_tokens(
+/// Convert a LanguageModelRequest to an Anthropic CountTokensRequest.
+pub fn into_anthropic_count_tokens_request(
     request: LanguageModelRequest,
-    cx: &App,
-) -> BoxFuture<'static, Result<u64>> {
-    cx.background_spawn(async move {
-        let messages = request.messages;
-        let mut tokens_from_images = 0;
-        let mut string_messages = Vec::with_capacity(messages.len());
-
-        for message in messages {
-            use language_model::MessageContent;
-
-            let mut string_contents = String::new();
-
-            for content in message.content {
-                match content {
-                    MessageContent::Text(text) => {
-                        string_contents.push_str(&text);
-                    }
-                    MessageContent::Thinking { .. } => {
-                        // Thinking blocks are not included in the input token count.
-                    }
-                    MessageContent::RedactedThinking(_) => {
-                        // Thinking blocks are not included in the input token count.
-                    }
-                    MessageContent::Image(image) => {
-                        tokens_from_images += image.estimate_tokens();
-                    }
-                    MessageContent::ToolUse(_tool_use) => {
-                        // TODO: Estimate token usage from tool uses.
-                    }
-                    MessageContent::ToolResult(tool_result) => match &tool_result.content {
-                        LanguageModelToolResultContent::Text(text) => {
-                            string_contents.push_str(text);
+    model: String,
+    mode: AnthropicModelMode,
+) -> CountTokensRequest {
+    let mut new_messages: Vec<anthropic::Message> = Vec::new();
+    let mut system_message = String::new();
+
+    for message in request.messages {
+        if message.contents_empty() {
+            continue;
+        }
+
+        match message.role {
+            Role::User | Role::Assistant => {
+                let anthropic_message_content: Vec<anthropic::RequestContent> = message
+                    .content
+                    .into_iter()
+                    .filter_map(|content| match content {
+                        MessageContent::Text(text) => {
+                            let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) {
+                                text.trim_end().to_string()
+                            } else {
+                                text
+                            };
+                            if !text.is_empty() {
+                                Some(anthropic::RequestContent::Text {
+                                    text,
+                                    cache_control: None,
+                                })
+                            } else {
+                                None
+                            }
+                        }
+                        MessageContent::Thinking {
+                            text: thinking,
+                            signature,
+                        } => {
+                            if !thinking.is_empty() {
+                                Some(anthropic::RequestContent::Thinking {
+                                    thinking,
+                                    signature: signature.unwrap_or_default(),
+                                    cache_control: None,
+                                })
+                            } else {
+                                None
+                            }
+                        }
+                        MessageContent::RedactedThinking(data) => {
+                            if !data.is_empty() {
+                                Some(anthropic::RequestContent::RedactedThinking { data })
+                            } else {
+                                None
+                            }
                         }
-                        LanguageModelToolResultContent::Image(image) => {
-                            tokens_from_images += image.estimate_tokens();
+                        MessageContent::Image(image) => Some(anthropic::RequestContent::Image {
+                            source: anthropic::ImageSource {
+                                source_type: "base64".to_string(),
+                                media_type: "image/png".to_string(),
+                                data: image.source.to_string(),
+                            },
+                            cache_control: None,
+                        }),
+                        MessageContent::ToolUse(tool_use) => {
+                            Some(anthropic::RequestContent::ToolUse {
+                                id: tool_use.id.to_string(),
+                                name: tool_use.name.to_string(),
+                                input: tool_use.input,
+                                cache_control: None,
+                            })
+                        }
+                        MessageContent::ToolResult(tool_result) => {
+                            Some(anthropic::RequestContent::ToolResult {
+                                tool_use_id: tool_result.tool_use_id.to_string(),
+                                is_error: tool_result.is_error,
+                                content: match tool_result.content {
+                                    LanguageModelToolResultContent::Text(text) => {
+                                        ToolResultContent::Plain(text.to_string())
+                                    }
+                                    LanguageModelToolResultContent::Image(image) => {
+                                        ToolResultContent::Multipart(vec![ToolResultPart::Image {
+                                            source: anthropic::ImageSource {
+                                                source_type: "base64".to_string(),
+                                                media_type: "image/png".to_string(),
+                                                data: image.source.to_string(),
+                                            },
+                                        }])
+                                    }
+                                },
+                                cache_control: None,
+                            })
                         }
-                    },
+                    })
+                    .collect();
+                let anthropic_role = match message.role {
+                    Role::User => anthropic::Role::User,
+                    Role::Assistant => anthropic::Role::Assistant,
+                    Role::System => unreachable!("System role should never occur here"),
+                };
+                if let Some(last_message) = new_messages.last_mut()
+                    && last_message.role == anthropic_role
+                {
+                    last_message.content.extend(anthropic_message_content);
+                    continue;
                 }
-            }
 
-            if !string_contents.is_empty() {
-                string_messages.push(tiktoken_rs::ChatCompletionRequestMessage {
-                    role: match message.role {
-                        Role::User => "user".into(),
-                        Role::Assistant => "assistant".into(),
-                        Role::System => "system".into(),
-                    },
-                    content: Some(string_contents),
-                    name: None,
-                    function_call: None,
+                new_messages.push(anthropic::Message {
+                    role: anthropic_role,
+                    content: anthropic_message_content,
                 });
             }
+            Role::System => {
+                if !system_message.is_empty() {
+                    system_message.push_str("\n\n");
+                }
+                system_message.push_str(&message.string_contents());
+            }
+        }
+    }
+
+    CountTokensRequest {
+        model,
+        messages: new_messages,
+        system: if system_message.is_empty() {
+            None
+        } else {
+            Some(anthropic::StringOrContents::String(system_message))
+        },
+        thinking: if request.thinking_allowed
+            && let AnthropicModelMode::Thinking { budget_tokens } = mode
+        {
+            Some(anthropic::Thinking::Enabled { budget_tokens })
+        } else {
+            None
+        },
+        tools: request
+            .tools
+            .into_iter()
+            .map(|tool| anthropic::Tool {
+                name: tool.name,
+                description: tool.description,
+                input_schema: tool.input_schema,
+            })
+            .collect(),
+        tool_choice: request.tool_choice.map(|choice| match choice {
+            LanguageModelToolChoice::Auto => anthropic::ToolChoice::Auto,
+            LanguageModelToolChoice::Any => anthropic::ToolChoice::Any,
+            LanguageModelToolChoice::None => anthropic::ToolChoice::None,
+        }),
+    }
+}
+
+/// Estimate tokens using tiktoken. Used as a fallback when the API is unavailable,
+/// or by providers (like Zed Cloud) that don't have direct Anthropic API access.
+pub fn count_anthropic_tokens_with_tiktoken(request: LanguageModelRequest) -> Result<u64> {
+    let messages = request.messages;
+    let mut tokens_from_images = 0;
+    let mut string_messages = Vec::with_capacity(messages.len());
+
+    for message in messages {
+        let mut string_contents = String::new();
+
+        for content in message.content {
+            match content {
+                MessageContent::Text(text) => {
+                    string_contents.push_str(&text);
+                }
+                MessageContent::Thinking { .. } => {
+                    // Thinking blocks are not included in the input token count.
+                }
+                MessageContent::RedactedThinking(_) => {
+                    // Thinking blocks are not included in the input token count.
+                }
+                MessageContent::Image(image) => {
+                    tokens_from_images += image.estimate_tokens();
+                }
+                MessageContent::ToolUse(_tool_use) => {
+                    // TODO: Estimate token usage from tool uses.
+                }
+                MessageContent::ToolResult(tool_result) => match &tool_result.content {
+                    LanguageModelToolResultContent::Text(text) => {
+                        string_contents.push_str(text);
+                    }
+                    LanguageModelToolResultContent::Image(image) => {
+                        tokens_from_images += image.estimate_tokens();
+                    }
+                },
+            }
+        }
+
+        if !string_contents.is_empty() {
+            string_messages.push(tiktoken_rs::ChatCompletionRequestMessage {
+                role: match message.role {
+                    Role::User => "user".into(),
+                    Role::Assistant => "assistant".into(),
+                    Role::System => "system".into(),
+                },
+                content: Some(string_contents),
+                name: None,
+                function_call: None,
+            });
         }
+    }
 
-        // Tiktoken doesn't yet support these models, so we manually use the
-        // same tokenizer as GPT-4.
-        tiktoken_rs::num_tokens_from_messages("gpt-4", &string_messages)
-            .map(|tokens| (tokens + tokens_from_images) as u64)
-    })
-    .boxed()
+    // Tiktoken doesn't yet support these models, so we manually use the
+    // same tokenizer as GPT-4.
+    tiktoken_rs::num_tokens_from_messages("gpt-4", &string_messages)
+        .map(|tokens| (tokens + tokens_from_images) as u64)
 }
 
 impl AnthropicModel {
@@ -386,7 +533,40 @@ impl LanguageModel for AnthropicModel {
         request: LanguageModelRequest,
         cx: &App,
     ) -> BoxFuture<'static, Result<u64>> {
-        count_anthropic_tokens(request, cx)
+        let http_client = self.http_client.clone();
+        let model_id = self.model.request_id().to_string();
+        let mode = self.model.mode();
+
+        let (api_key, api_url) = self.state.read_with(cx, |state, cx| {
+            let api_url = AnthropicLanguageModelProvider::api_url(cx);
+            (
+                state.api_key_state.key(&api_url).map(|k| k.to_string()),
+                api_url.to_string(),
+            )
+        });
+
+        async move {
+            // If no API key, fall back to tiktoken estimation
+            let Some(api_key) = api_key else {
+                return count_anthropic_tokens_with_tiktoken(request);
+            };
+
+            let count_request =
+                into_anthropic_count_tokens_request(request.clone(), model_id, mode);
+
+            match anthropic::count_tokens(http_client.as_ref(), &api_url, &api_key, count_request)
+                .await
+            {
+                Ok(response) => Ok(response.input_tokens),
+                Err(err) => {
+                    log::error!(
+                        "Anthropic count_tokens API failed, falling back to tiktoken: {err:?}"
+                    );
+                    count_anthropic_tokens_with_tiktoken(request)
+                }
+            }
+        }
+        .boxed()
     }
 
     fn stream_completion(

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

@@ -5,7 +5,7 @@ use std::sync::Arc;
 use anyhow::{Context as _, Result, anyhow};
 use aws_config::stalled_stream_protection::StalledStreamProtectionConfig;
 use aws_config::{BehaviorVersion, Region};
-use aws_credential_types::Credentials;
+use aws_credential_types::{Credentials, Token};
 use aws_http_client::AwsHttpClient;
 use bedrock::bedrock_client::Client as BedrockClient;
 use bedrock::bedrock_client::config::timeout::TimeoutConfig;
@@ -30,18 +30,19 @@ use gpui::{
 use gpui_tokio::Tokio;
 use http_client::HttpClient;
 use language_model::{
-    AuthenticateError, LanguageModel, LanguageModelCacheConfiguration,
+    AuthenticateError, EnvVar, LanguageModel, LanguageModelCacheConfiguration,
     LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
     LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
     LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
     LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, RateLimiter, Role,
-    TokenUsage,
+    TokenUsage, env_var,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore};
 use smol::lock::OnceCell;
+use std::sync::LazyLock;
 use strum::{EnumIter, IntoEnumIterator, IntoStaticStr};
 use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
 use ui_input::InputField;
@@ -54,12 +55,52 @@ actions!(bedrock, [Tab, TabPrev]);
 const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("amazon-bedrock");
 const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Amazon Bedrock");
 
+/// Credentials stored in the keychain for static authentication.
+/// Region is handled separately since it's orthogonal to auth method.
 #[derive(Default, Clone, Deserialize, Serialize, PartialEq, Debug)]
 pub struct BedrockCredentials {
     pub access_key_id: String,
     pub secret_access_key: String,
     pub session_token: Option<String>,
-    pub region: String,
+    pub bearer_token: Option<String>,
+}
+
+/// Resolved authentication configuration for Bedrock.
+/// Settings take priority over UX-provided credentials.
+#[derive(Clone, Debug, PartialEq)]
+pub enum BedrockAuth {
+    /// Use default AWS credential provider chain (IMDSv2, PodIdentity, env vars, etc.)
+    Automatic,
+    /// Use AWS named profile from ~/.aws/credentials or ~/.aws/config
+    NamedProfile { profile_name: String },
+    /// Use AWS SSO profile
+    SingleSignOn { profile_name: String },
+    /// Use IAM credentials (access key + secret + optional session token)
+    IamCredentials {
+        access_key_id: String,
+        secret_access_key: String,
+        session_token: Option<String>,
+    },
+    /// Use Bedrock API Key (bearer token authentication)
+    ApiKey { api_key: String },
+}
+
+impl BedrockCredentials {
+    /// Convert stored credentials to the appropriate auth variant.
+    /// Prefers API key if present, otherwise uses IAM credentials.
+    fn into_auth(self) -> Option<BedrockAuth> {
+        if let Some(api_key) = self.bearer_token.filter(|t| !t.is_empty()) {
+            Some(BedrockAuth::ApiKey { api_key })
+        } else if !self.access_key_id.is_empty() && !self.secret_access_key.is_empty() {
+            Some(BedrockAuth::IamCredentials {
+                access_key_id: self.access_key_id,
+                secret_access_key: self.secret_access_key,
+                session_token: self.session_token.filter(|t| !t.is_empty()),
+            })
+        } else {
+            None
+        }
+    }
 }
 
 #[derive(Default, Clone, Debug, PartialEq)]
@@ -79,6 +120,8 @@ pub enum BedrockAuthMethod {
     NamedProfile,
     #[serde(rename = "sso")]
     SingleSignOn,
+    #[serde(rename = "api_key")]
+    ApiKey,
     /// IMDSv2, PodIdentity, env vars, etc.
     #[serde(rename = "default")]
     Automatic,
@@ -90,6 +133,7 @@ impl From<settings::BedrockAuthMethodContent> for BedrockAuthMethod {
             settings::BedrockAuthMethodContent::SingleSignOn => BedrockAuthMethod::SingleSignOn,
             settings::BedrockAuthMethodContent::Automatic => BedrockAuthMethod::Automatic,
             settings::BedrockAuthMethodContent::NamedProfile => BedrockAuthMethod::NamedProfile,
+            settings::BedrockAuthMethodContent::ApiKey => BedrockAuthMethod::ApiKey,
         }
     }
 }
@@ -130,23 +174,26 @@ impl From<BedrockModelMode> for ModelMode {
 const AMAZON_AWS_URL: &str = "https://amazonaws.com";
 
 // These environment variables all use a `ZED_` prefix because we don't want to overwrite the user's AWS credentials.
-const ZED_BEDROCK_ACCESS_KEY_ID_VAR: &str = "ZED_ACCESS_KEY_ID";
-const ZED_BEDROCK_SECRET_ACCESS_KEY_VAR: &str = "ZED_SECRET_ACCESS_KEY";
-const ZED_BEDROCK_SESSION_TOKEN_VAR: &str = "ZED_SESSION_TOKEN";
-const ZED_AWS_PROFILE_VAR: &str = "ZED_AWS_PROFILE";
-const ZED_BEDROCK_REGION_VAR: &str = "ZED_AWS_REGION";
-const ZED_AWS_CREDENTIALS_VAR: &str = "ZED_AWS_CREDENTIALS";
-const ZED_AWS_ENDPOINT_VAR: &str = "ZED_AWS_ENDPOINT";
+static ZED_BEDROCK_ACCESS_KEY_ID_VAR: LazyLock<EnvVar> = env_var!("ZED_ACCESS_KEY_ID");
+static ZED_BEDROCK_SECRET_ACCESS_KEY_VAR: LazyLock<EnvVar> = env_var!("ZED_SECRET_ACCESS_KEY");
+static ZED_BEDROCK_SESSION_TOKEN_VAR: LazyLock<EnvVar> = env_var!("ZED_SESSION_TOKEN");
+static ZED_AWS_PROFILE_VAR: LazyLock<EnvVar> = env_var!("ZED_AWS_PROFILE");
+static ZED_BEDROCK_REGION_VAR: LazyLock<EnvVar> = env_var!("ZED_AWS_REGION");
+static ZED_AWS_ENDPOINT_VAR: LazyLock<EnvVar> = env_var!("ZED_AWS_ENDPOINT");
+static ZED_BEDROCK_BEARER_TOKEN_VAR: LazyLock<EnvVar> = env_var!("ZED_BEDROCK_BEARER_TOKEN");
 
 pub struct State {
-    credentials: Option<BedrockCredentials>,
+    /// The resolved authentication method. Settings take priority over UX credentials.
+    auth: Option<BedrockAuth>,
+    /// Raw settings from settings.json
     settings: Option<AmazonBedrockSettings>,
+    /// Whether credentials came from environment variables (only relevant for static credentials)
     credentials_from_env: bool,
     _subscription: Subscription,
 }
 
 impl State {
-    fn reset_credentials(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
+    fn reset_auth(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
         let credentials_provider = <dyn CredentialsProvider>::global(cx);
         cx.spawn(async move |this, cx| {
             credentials_provider
@@ -154,19 +201,19 @@ impl State {
                 .await
                 .log_err();
             this.update(cx, |this, cx| {
-                this.credentials = None;
+                this.auth = None;
                 this.credentials_from_env = false;
-                this.settings = None;
                 cx.notify();
             })
         })
     }
 
-    fn set_credentials(
+    fn set_static_credentials(
         &mut self,
         credentials: BedrockCredentials,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
+        let auth = credentials.clone().into_auth();
         let credentials_provider = <dyn CredentialsProvider>::global(cx);
         cx.spawn(async move |this, cx| {
             credentials_provider
@@ -178,50 +225,131 @@ impl State {
                 )
                 .await?;
             this.update(cx, |this, cx| {
-                this.credentials = Some(credentials);
+                this.auth = auth;
+                this.credentials_from_env = false;
                 cx.notify();
             })
         })
     }
 
     fn is_authenticated(&self) -> bool {
-        let derived = self
-            .settings
-            .as_ref()
-            .and_then(|s| s.authentication_method.as_ref());
-        let creds = self.credentials.as_ref();
-
-        derived.is_some() || creds.is_some()
+        self.auth.is_some()
     }
 
+    /// Resolve authentication. Settings take priority over UX-provided credentials.
     fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
         if self.is_authenticated() {
             return Task::ready(Ok(()));
         }
 
+        // Step 1: Check if settings specify an auth method (enterprise control)
+        if let Some(settings) = &self.settings {
+            if let Some(method) = &settings.authentication_method {
+                let profile_name = settings
+                    .profile_name
+                    .clone()
+                    .unwrap_or_else(|| "default".to_string());
+
+                let auth = match method {
+                    BedrockAuthMethod::Automatic => BedrockAuth::Automatic,
+                    BedrockAuthMethod::NamedProfile => BedrockAuth::NamedProfile { profile_name },
+                    BedrockAuthMethod::SingleSignOn => BedrockAuth::SingleSignOn { profile_name },
+                    BedrockAuthMethod::ApiKey => {
+                        // ApiKey method means "use static credentials from keychain/env"
+                        // Fall through to load them below
+                        return self.load_static_credentials(cx);
+                    }
+                };
+
+                return cx.spawn(async move |this, cx| {
+                    this.update(cx, |this, cx| {
+                        this.auth = Some(auth);
+                        this.credentials_from_env = false;
+                        cx.notify();
+                    })?;
+                    Ok(())
+                });
+            }
+        }
+
+        // Step 2: No settings auth method - try to load static credentials
+        self.load_static_credentials(cx)
+    }
+
+    /// Load static credentials from environment variables or keychain.
+    fn load_static_credentials(
+        &self,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<(), AuthenticateError>> {
         let credentials_provider = <dyn CredentialsProvider>::global(cx);
         cx.spawn(async move |this, cx| {
-            let (credentials, from_env) =
-                if let Ok(credentials) = std::env::var(ZED_AWS_CREDENTIALS_VAR) {
-                    (credentials, true)
-                } else {
-                    let (_, credentials) = credentials_provider
-                        .read_credentials(AMAZON_AWS_URL, cx)
-                        .await?
-                        .ok_or_else(|| AuthenticateError::CredentialsNotFound)?;
+            // Try environment variables first
+            let (auth, from_env) = if let Some(bearer_token) = &ZED_BEDROCK_BEARER_TOKEN_VAR.value {
+                if !bearer_token.is_empty() {
                     (
-                        String::from_utf8(credentials)
-                            .context("invalid {PROVIDER_NAME} credentials")?,
-                        false,
+                        Some(BedrockAuth::ApiKey {
+                            api_key: bearer_token.to_string(),
+                        }),
+                        true,
                     )
-                };
+                } else {
+                    (None, false)
+                }
+            } else if let Some(access_key_id) = &ZED_BEDROCK_ACCESS_KEY_ID_VAR.value {
+                if let Some(secret_access_key) = &ZED_BEDROCK_SECRET_ACCESS_KEY_VAR.value {
+                    if !access_key_id.is_empty() && !secret_access_key.is_empty() {
+                        let session_token = ZED_BEDROCK_SESSION_TOKEN_VAR
+                            .value
+                            .as_deref()
+                            .filter(|s| !s.is_empty())
+                            .map(|s| s.to_string());
+                        (
+                            Some(BedrockAuth::IamCredentials {
+                                access_key_id: access_key_id.to_string(),
+                                secret_access_key: secret_access_key.to_string(),
+                                session_token,
+                            }),
+                            true,
+                        )
+                    } else {
+                        (None, false)
+                    }
+                } else {
+                    (None, false)
+                }
+            } else {
+                (None, false)
+            };
+
+            // If we got auth from env vars, use it
+            if let Some(auth) = auth {
+                this.update(cx, |this, cx| {
+                    this.auth = Some(auth);
+                    this.credentials_from_env = from_env;
+                    cx.notify();
+                })?;
+                return Ok(());
+            }
+
+            // Try keychain
+            let (_, credentials_bytes) = credentials_provider
+                .read_credentials(AMAZON_AWS_URL, cx)
+                .await?
+                .ok_or(AuthenticateError::CredentialsNotFound)?;
+
+            let credentials_str = String::from_utf8(credentials_bytes)
+                .context("invalid {PROVIDER_NAME} credentials")?;
 
             let credentials: BedrockCredentials =
-                serde_json::from_str(&credentials).context("failed to parse credentials")?;
+                serde_json::from_str(&credentials_str).context("failed to parse credentials")?;
+
+            let auth = credentials
+                .into_auth()
+                .ok_or(AuthenticateError::CredentialsNotFound)?;
 
             this.update(cx, |this, cx| {
-                this.credentials = Some(credentials);
-                this.credentials_from_env = from_env;
+                this.auth = Some(auth);
+                this.credentials_from_env = false;
                 cx.notify();
             })?;
 
@@ -229,15 +357,19 @@ impl State {
         })
     }
 
+    /// Get the resolved region. Checks env var, then settings, then defaults to us-east-1.
     fn get_region(&self) -> String {
-        // Get region - from credentials or directly from settings
-        let credentials_region = self.credentials.as_ref().map(|s| s.region.clone());
-        let settings_region = self.settings.as_ref().and_then(|s| s.region.clone());
-
-        // Use credentials region if available, otherwise use settings region, finally fall back to default
-        credentials_region
-            .or(settings_region)
-            .unwrap_or(String::from("us-east-1"))
+        // Priority: env var > settings > default
+        if let Some(region) = ZED_BEDROCK_REGION_VAR.value.as_deref() {
+            if !region.is_empty() {
+                return region.to_string();
+            }
+        }
+
+        self.settings
+            .as_ref()
+            .and_then(|s| s.region.clone())
+            .unwrap_or_else(|| "us-east-1".to_string())
     }
 
     fn get_allow_global(&self) -> bool {
@@ -257,7 +389,7 @@ pub struct BedrockLanguageModelProvider {
 impl BedrockLanguageModelProvider {
     pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
         let state = cx.new(|cx| State {
-            credentials: None,
+            auth: None,
             settings: Some(AllLanguageModelSettings::get_global(cx).bedrock.clone()),
             credentials_from_env: false,
             _subscription: cx.observe_global::<SettingsStore>(|_, cx| {
@@ -266,7 +398,7 @@ impl BedrockLanguageModelProvider {
         });
 
         Self {
-            http_client: AwsHttpClient::new(http_client.clone()),
+            http_client: AwsHttpClient::new(http_client),
             handle: Tokio::handle(cx),
             state,
         }
@@ -312,7 +444,6 @@ impl LanguageModelProvider for BedrockLanguageModelProvider {
 
         for model in bedrock::Model::iter() {
             if !matches!(model, bedrock::Model::Custom { .. }) {
-                // TODO: Sonnet 3.7 vs. 3.7 Thinking bug is here.
                 models.insert(model.id().to_string(), model);
             }
         }
@@ -366,8 +497,7 @@ impl LanguageModelProvider for BedrockLanguageModelProvider {
     }
 
     fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> {
-        self.state
-            .update(cx, |state, cx| state.reset_credentials(cx))
+        self.state.update(cx, |state, cx| state.reset_auth(cx))
     }
 }
 
@@ -393,25 +523,11 @@ impl BedrockModel {
     fn get_or_init_client(&self, cx: &AsyncApp) -> anyhow::Result<&BedrockClient> {
         self.client
             .get_or_try_init_blocking(|| {
-                let (auth_method, credentials, endpoint, region, settings) =
-                    cx.read_entity(&self.state, |state, _cx| {
-                        let auth_method = state
-                            .settings
-                            .as_ref()
-                            .and_then(|s| s.authentication_method.clone());
-
-                        let endpoint = state.settings.as_ref().and_then(|s| s.endpoint.clone());
-
-                        let region = state.get_region();
-
-                        (
-                            auth_method,
-                            state.credentials.clone(),
-                            endpoint,
-                            region,
-                            state.settings.clone(),
-                        )
-                    })?;
+                let (auth, endpoint, region) = cx.read_entity(&self.state, |state, _cx| {
+                    let endpoint = state.settings.as_ref().and_then(|s| s.endpoint.clone());
+                    let region = state.get_region();
+                    (state.auth.clone(), endpoint, region)
+                })?;
 
                 let mut config_builder = aws_config::defaults(BehaviorVersion::latest())
                     .stalled_stream_protection(StalledStreamProtectionConfig::disabled())
@@ -425,37 +541,39 @@ impl BedrockModel {
                     config_builder = config_builder.endpoint_url(endpoint_url);
                 }
 
-                match auth_method {
-                    None => {
-                        if let Some(creds) = credentials {
-                            let aws_creds = Credentials::new(
-                                creds.access_key_id,
-                                creds.secret_access_key,
-                                creds.session_token,
-                                None,
-                                "zed-bedrock-provider",
-                            );
-                            config_builder = config_builder.credentials_provider(aws_creds);
-                        }
+                match auth {
+                    Some(BedrockAuth::Automatic) | None => {
+                        // Use default AWS credential provider chain
                     }
-                    Some(BedrockAuthMethod::NamedProfile)
-                    | Some(BedrockAuthMethod::SingleSignOn) => {
-                        // Currently NamedProfile and SSO behave the same way but only the instructions change
-                        // Until we support BearerAuth through SSO, this will not change.
-                        let profile_name = settings
-                            .and_then(|s| s.profile_name)
-                            .unwrap_or_else(|| "default".to_string());
-
+                    Some(BedrockAuth::NamedProfile { profile_name })
+                    | Some(BedrockAuth::SingleSignOn { profile_name }) => {
                         if !profile_name.is_empty() {
                             config_builder = config_builder.profile_name(profile_name);
                         }
                     }
-                    Some(BedrockAuthMethod::Automatic) => {
-                        // Use default credential provider chain
+                    Some(BedrockAuth::IamCredentials {
+                        access_key_id,
+                        secret_access_key,
+                        session_token,
+                    }) => {
+                        let aws_creds = Credentials::new(
+                            access_key_id,
+                            secret_access_key,
+                            session_token,
+                            None,
+                            "zed-bedrock-provider",
+                        );
+                        config_builder = config_builder.credentials_provider(aws_creds);
+                    }
+                    Some(BedrockAuth::ApiKey { api_key }) => {
+                        config_builder = config_builder
+                            .auth_scheme_preference(["httpBearerAuth".into()]) // https://github.com/smithy-lang/smithy-rs/pull/4241
+                            .token_provider(Token::new(api_key, None));
                     }
                 }
 
                 let config = self.handle.block_on(config_builder.load());
+
                 anyhow::Ok(BedrockClient::new(&config))
             })
             .context("initializing Bedrock client")?;
@@ -1024,7 +1142,7 @@ struct ConfigurationView {
     access_key_id_editor: Entity<InputField>,
     secret_access_key_editor: Entity<InputField>,
     session_token_editor: Entity<InputField>,
-    region_editor: Entity<InputField>,
+    bearer_token_editor: Entity<InputField>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
     focus_handle: FocusHandle,
@@ -1035,7 +1153,7 @@ impl ConfigurationView {
     const PLACEHOLDER_SECRET_ACCESS_KEY_TEXT: &'static str =
         "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
     const PLACEHOLDER_SESSION_TOKEN_TEXT: &'static str = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
-    const PLACEHOLDER_REGION: &'static str = "us-east-1";
+    const PLACEHOLDER_BEARER_TOKEN_TEXT: &'static str = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
 
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let focus_handle = cx.focus_handle();
@@ -1066,9 +1184,9 @@ impl ConfigurationView {
                 .tab_stop(true)
         });
 
-        let region_editor = cx.new(|cx| {
-            InputField::new(window, cx, Self::PLACEHOLDER_REGION)
-                .label("Region")
+        let bearer_token_editor = cx.new(|cx| {
+            InputField::new(window, cx, Self::PLACEHOLDER_BEARER_TOKEN_TEXT)
+                .label("Bedrock API Key")
                 .tab_index(3)
                 .tab_stop(true)
         });
@@ -1095,7 +1213,7 @@ impl ConfigurationView {
             access_key_id_editor,
             secret_access_key_editor,
             session_token_editor,
-            region_editor,
+            bearer_token_editor,
             state,
             load_credentials_task,
             focus_handle,
@@ -1131,25 +1249,30 @@ impl ConfigurationView {
         } else {
             Some(session_token)
         };
-        let region = self.region_editor.read(cx).text(cx).trim().to_string();
-        let region = if region.is_empty() {
-            "us-east-1".to_string()
+        let bearer_token = self
+            .bearer_token_editor
+            .read(cx)
+            .text(cx)
+            .trim()
+            .to_string();
+        let bearer_token = if bearer_token.is_empty() {
+            None
         } else {
-            region
+            Some(bearer_token)
         };
 
         let state = self.state.clone();
         cx.spawn(async move |_, cx| {
             state
                 .update(cx, |state, cx| {
-                    let credentials: BedrockCredentials = BedrockCredentials {
-                        region: region.clone(),
-                        access_key_id: access_key_id.clone(),
-                        secret_access_key: secret_access_key.clone(),
-                        session_token: session_token.clone(),
+                    let credentials = BedrockCredentials {
+                        access_key_id,
+                        secret_access_key,
+                        session_token,
+                        bearer_token,
                     };
 
-                    state.set_credentials(credentials, cx)
+                    state.set_static_credentials(credentials, cx)
                 })?
                 .await
         })
@@ -1163,41 +1286,39 @@ impl ConfigurationView {
             .update(cx, |editor, cx| editor.set_text("", window, cx));
         self.session_token_editor
             .update(cx, |editor, cx| editor.set_text("", window, cx));
-        self.region_editor
+        self.bearer_token_editor
             .update(cx, |editor, cx| editor.set_text("", window, cx));
 
         let state = self.state.clone();
-        cx.spawn(async move |_, cx| {
-            state
-                .update(cx, |state, cx| state.reset_credentials(cx))?
-                .await
-        })
-        .detach_and_log_err(cx);
+        cx.spawn(async move |_, cx| state.update(cx, |state, cx| state.reset_auth(cx))?.await)
+            .detach_and_log_err(cx);
     }
 
     fn should_render_editor(&self, cx: &Context<Self>) -> bool {
         self.state.read(cx).is_authenticated()
     }
 
-    fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
-        window.focus_next();
+    fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+        window.focus_next(cx);
     }
 
     fn on_tab_prev(
         &mut self,
         _: &menu::SelectPrevious,
         window: &mut Window,
-        _: &mut Context<Self>,
+        cx: &mut Context<Self>,
     ) {
-        window.focus_prev();
+        window.focus_prev(cx);
     }
 }
 
 impl Render for ConfigurationView {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let env_var_set = self.state.read(cx).credentials_from_env;
-        let bedrock_settings = self.state.read(cx).settings.as_ref();
-        let bedrock_method = bedrock_settings
+        let state = self.state.read(cx);
+        let env_var_set = state.credentials_from_env;
+        let auth = state.auth.clone();
+        let settings_auth_method = state
+            .settings
             .as_ref()
             .and_then(|s| s.authentication_method.clone());
 
@@ -1205,34 +1326,62 @@ impl Render for ConfigurationView {
             return div().child(Label::new("Loading credentials...")).into_any();
         }
 
-        let configured_label = if env_var_set {
-            format!(
-                "Access Key ID is set in {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, Secret Key is set in {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, Region is set in {ZED_BEDROCK_REGION_VAR} environment variables."
-            )
-        } else {
-            match bedrock_method {
-                Some(BedrockAuthMethod::Automatic) => "You are using automatic credentials.".into(),
-                Some(BedrockAuthMethod::NamedProfile) => "You are using named profile.".into(),
-                Some(BedrockAuthMethod::SingleSignOn) => {
-                    "You are using a single sign on profile.".into()
-                }
-                None => "You are using static credentials.".into(),
+        let configured_label = match &auth {
+            Some(BedrockAuth::Automatic) => {
+                "Using automatic credentials (AWS default chain)".into()
+            }
+            Some(BedrockAuth::NamedProfile { profile_name }) => {
+                format!("Using AWS profile: {profile_name}")
+            }
+            Some(BedrockAuth::SingleSignOn { profile_name }) => {
+                format!("Using AWS SSO profile: {profile_name}")
+            }
+            Some(BedrockAuth::IamCredentials { .. }) if env_var_set => {
+                format!(
+                    "Using IAM credentials from {} and {} environment variables",
+                    ZED_BEDROCK_ACCESS_KEY_ID_VAR.name, ZED_BEDROCK_SECRET_ACCESS_KEY_VAR.name
+                )
+            }
+            Some(BedrockAuth::IamCredentials { .. }) => "Using IAM credentials".into(),
+            Some(BedrockAuth::ApiKey { .. }) if env_var_set => {
+                format!(
+                    "Using Bedrock API Key from {} environment variable",
+                    ZED_BEDROCK_BEARER_TOKEN_VAR.name
+                )
             }
+            Some(BedrockAuth::ApiKey { .. }) => "Using Bedrock API Key".into(),
+            None => "Not authenticated".into(),
         };
 
+        // Determine if credentials can be reset
+        // Settings-derived auth (non-ApiKey) cannot be reset from UI
+        let is_settings_derived = matches!(
+            settings_auth_method,
+            Some(BedrockAuthMethod::Automatic)
+                | Some(BedrockAuthMethod::NamedProfile)
+                | Some(BedrockAuthMethod::SingleSignOn)
+        );
+
         let tooltip_label = if env_var_set {
             Some(format!(
-                "To reset your credentials, unset the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, and {ZED_BEDROCK_REGION_VAR} environment variables."
+                "To reset your credentials, unset the {}, {}, and {} or {} environment variables.",
+                ZED_BEDROCK_ACCESS_KEY_ID_VAR.name,
+                ZED_BEDROCK_SECRET_ACCESS_KEY_VAR.name,
+                ZED_BEDROCK_SESSION_TOKEN_VAR.name,
+                ZED_BEDROCK_BEARER_TOKEN_VAR.name
             ))
-        } else if bedrock_method.is_some() {
-            Some("You cannot reset credentials as they're being derived, check Zed settings to understand how.".to_string())
+        } else if is_settings_derived {
+            Some(
+                "Authentication method is configured in settings. Edit settings.json to change."
+                    .to_string(),
+            )
         } else {
             None
         };
 
         if self.should_render_editor(cx) {
             return ConfiguredApiCard::new(configured_label)
-                .disabled(env_var_set || bedrock_method.is_some())
+                .disabled(env_var_set || is_settings_derived)
                 .on_click(cx.listener(|this, _, window, cx| this.reset_credentials(window, cx)))
                 .when_some(tooltip_label, |this, label| this.tooltip_label(label))
                 .into_any_element();
@@ -1262,7 +1411,7 @@ impl Render for ConfigurationView {
             .child(self.render_static_credentials_ui())
             .child(
                 Label::new(
-                    format!("You can also assign the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR} AND {ZED_BEDROCK_REGION_VAR} environment variables and restart Zed."),
+                    format!("You can also assign the {}, {} AND {} environment variables (or {} for Bedrock API Key authentication) and restart Zed.", ZED_BEDROCK_ACCESS_KEY_ID_VAR.name, ZED_BEDROCK_SECRET_ACCESS_KEY_VAR.name, ZED_BEDROCK_REGION_VAR.name, ZED_BEDROCK_BEARER_TOKEN_VAR.name),
                 )
                     .size(LabelSize::Small)
                     .color(Color::Muted)
@@ -1270,7 +1419,7 @@ impl Render for ConfigurationView {
             )
             .child(
                 Label::new(
-                    format!("Optionally, if your environment uses AWS CLI profiles, you can set {ZED_AWS_PROFILE_VAR}; if it requires a custom endpoint, you can set {ZED_AWS_ENDPOINT_VAR}; and if it requires a Session Token, you can set {ZED_BEDROCK_SESSION_TOKEN_VAR}."),
+                    format!("Optionally, if your environment uses AWS CLI profiles, you can set {}; if it requires a custom endpoint, you can set {}; and if it requires a Session Token, you can set {}.", ZED_AWS_PROFILE_VAR.name, ZED_AWS_ENDPOINT_VAR.name, ZED_BEDROCK_SESSION_TOKEN_VAR.name),
                 )
                     .size(LabelSize::Small)
                     .color(Color::Muted),
@@ -1292,31 +1441,47 @@ impl ConfigurationView {
             )
             .child(
                 Label::new(
-                    "This method uses your AWS access key ID and secret access key directly.",
+                    "This method uses your AWS access key ID and secret access key, or a Bedrock API Key.",
                 )
             )
             .child(
                 List::new()
                     .child(
                         ListBulletItem::new("")
-                            .child(Label::new("Create an IAM user in the AWS console with programmatic access"))
+                            .child(Label::new("For access keys: Create an IAM user in the AWS console with programmatic access"))
                             .child(ButtonLink::new("IAM Console", "https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/users"))
                     )
                     .child(
                         ListBulletItem::new("")
-                            .child(Label::new("Attach the necessary Bedrock permissions to this"))
-                            .child(ButtonLink::new("user", "https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"))
+                            .child(Label::new("For Bedrock API Keys: Generate an API key from the"))
+                            .child(ButtonLink::new("Bedrock Console", "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys-use.html"))
                     )
                     .child(
-                        ListBulletItem::new("Copy the access key ID and secret access key when provided")
+                        ListBulletItem::new("")
+                            .child(Label::new("Attach the necessary Bedrock permissions to this"))
+                            .child(ButtonLink::new("user", "https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"))
                     )
                     .child(
-                        ListBulletItem::new("Enter these credentials below")
-                    )
+                        ListBulletItem::new("Enter either access keys OR a Bedrock API Key below (not both)")
+                    ),
             )
             .child(self.access_key_id_editor.clone())
             .child(self.secret_access_key_editor.clone())
             .child(self.session_token_editor.clone())
-            .child(self.region_editor.clone())
+            .child(
+                Label::new("OR")
+                    .size(LabelSize::Default)
+                    .weight(FontWeight::BOLD)
+                    .my_1(),
+            )
+            .child(self.bearer_token_editor.clone())
+            .child(
+                Label::new(
+                    format!("Region is configured via {} environment variable or settings.json (defaults to us-east-1).", ZED_BEDROCK_REGION_VAR.name),
+                )
+                    .size(LabelSize::Small)
+                    .color(Color::Muted)
+                    .mt_2(),
+            )
     }
 }

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

@@ -42,7 +42,9 @@ use thiserror::Error;
 use ui::{TintColor, prelude::*};
 use util::{ResultExt as _, maybe};
 
-use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, into_anthropic};
+use crate::provider::anthropic::{
+    AnthropicEventMapper, count_anthropic_tokens_with_tiktoken, into_anthropic,
+};
 use crate::provider::google::{GoogleEventMapper, into_google};
 use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
 use crate::provider::x_ai::count_xai_tokens;
@@ -667,9 +669,9 @@ impl LanguageModel for CloudLanguageModel {
         cx: &App,
     ) -> BoxFuture<'static, Result<u64>> {
         match self.model.provider {
-            cloud_llm_client::LanguageModelProvider::Anthropic => {
-                count_anthropic_tokens(request, cx)
-            }
+            cloud_llm_client::LanguageModelProvider::Anthropic => cx
+                .background_spawn(async move { count_anthropic_tokens_with_tiktoken(request) })
+                .boxed(),
             cloud_llm_client::LanguageModelProvider::OpenAi => {
                 let model = match open_ai::Model::from_id(&self.model.id.0) {
                     Ok(model) => model,

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

@@ -20,7 +20,7 @@ use settings::{Settings, SettingsStore};
 use std::pin::Pin;
 use std::str::FromStr;
 use std::{collections::BTreeMap, sync::Arc};
-use ui::{ButtonLike, Indicator, InlineCode, List, ListBulletItem, prelude::*};
+use ui::{ButtonLike, Indicator, List, ListBulletItem, prelude::*};
 use util::ResultExt;
 
 use crate::AllLanguageModelSettings;
@@ -691,7 +691,7 @@ impl Render for ConfigurationView {
                             .child(
                                 ListBulletItem::new("")
                                     .child(Label::new("To get your first model, try running"))
-                                    .child(InlineCode::new("lms get qwen2.5-coder-7b")),
+                                    .child(Label::new("lms get qwen2.5-coder-7b").inline_code(cx)),
                             ),
                     ),
                 )

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

@@ -23,8 +23,8 @@ use std::sync::LazyLock;
 use std::sync::atomic::{AtomicU64, Ordering};
 use std::{collections::HashMap, sync::Arc};
 use ui::{
-    ButtonLike, ButtonLink, ConfiguredApiCard, ElevationIndex, InlineCode, List, ListBulletItem,
-    Tooltip, prelude::*,
+    ButtonLike, ButtonLink, ConfiguredApiCard, ElevationIndex, List, ListBulletItem, Tooltip,
+    prelude::*,
 };
 use ui_input::InputField;
 
@@ -724,7 +724,7 @@ impl ConfigurationView {
         cx.notify();
     }
 
-    fn render_instructions() -> Div {
+    fn render_instructions(cx: &mut Context<Self>) -> Div {
         v_flex()
             .gap_2()
             .child(Label::new(
@@ -742,7 +742,7 @@ impl ConfigurationView {
                     .child(
                         ListBulletItem::new("")
                             .child(Label::new("Start Ollama and download a model:"))
-                            .child(InlineCode::new("ollama run gpt-oss:20b")),
+                            .child(Label::new("ollama run gpt-oss:20b").inline_code(cx)),
                     )
                     .child(ListBulletItem::new(
                         "Click 'Connect' below to start using Ollama in Zed",
@@ -833,7 +833,7 @@ impl Render for ConfigurationView {
 
         v_flex()
             .gap_2()
-            .child(Self::render_instructions())
+            .child(Self::render_instructions(cx))
             .child(self.render_api_url_editor(cx))
             .child(self.render_api_key_editor(cx))
             .child(

crates/language_tools/src/lsp_log_view.rs πŸ”—

@@ -125,7 +125,7 @@ pub fn init(on_headless_host: bool, cx: &mut App) {
                     let server_id = server.server_id();
                     let weak_lsp_store = cx.weak_entity();
                     log_store.copilot_log_subscription =
-                        Some(server.on_notification::<copilot::request::LogMessage, _>(
+                        Some(server.on_notification::<lsp::notification::LogMessage, _>(
                             move |params, cx| {
                                 weak_lsp_store
                                     .update(cx, |lsp_store, cx| {
@@ -269,7 +269,7 @@ impl LspLogView {
 
         let focus_handle = cx.focus_handle();
         let focus_subscription = cx.on_focus(&focus_handle, window, |log_view, window, cx| {
-            window.focus(&log_view.editor.focus_handle(cx));
+            window.focus(&log_view.editor.focus_handle(cx), cx);
         });
 
         cx.on_release(|log_view, cx| {
@@ -462,7 +462,7 @@ impl LspLogView {
             self.editor_subscriptions = editor_subscriptions;
             cx.notify();
         }
-        self.editor.read(cx).focus_handle(cx).focus(window);
+        self.editor.read(cx).focus_handle(cx).focus(window, cx);
         self.log_store.update(cx, |log_store, cx| {
             let state = log_store.get_language_server_state(server_id)?;
             state.toggled_log_kind = Some(LogKind::Logs);
@@ -494,7 +494,7 @@ impl LspLogView {
             cx.notify();
         }
 
-        self.editor.read(cx).focus_handle(cx).focus(window);
+        self.editor.read(cx).focus_handle(cx).focus(window, cx);
     }
 
     fn show_trace_for_server(
@@ -528,7 +528,7 @@ impl LspLogView {
             });
             cx.notify();
         }
-        self.editor.read(cx).focus_handle(cx).focus(window);
+        self.editor.read(cx).focus_handle(cx).focus(window, cx);
     }
 
     fn show_rpc_trace_for_server(
@@ -572,7 +572,7 @@ impl LspLogView {
             cx.notify();
         }
 
-        self.editor.read(cx).focus_handle(cx).focus(window);
+        self.editor.read(cx).focus_handle(cx).focus(window, cx);
     }
 
     fn toggle_rpc_trace_for_server(
@@ -660,7 +660,7 @@ impl LspLogView {
         self.editor = editor;
         self.editor_subscriptions = editor_subscriptions;
         cx.notify();
-        self.editor.read(cx).focus_handle(cx).focus(window);
+        self.editor.read(cx).focus_handle(cx).focus(window, cx);
         self.log_store.update(cx, |log_store, cx| {
             let state = log_store.get_language_server_state(server_id)?;
             if let Some(log_kind) = state.toggled_log_kind.take() {
@@ -1314,7 +1314,7 @@ impl LspLogToolbarItemView {
                     log_view.show_rpc_trace_for_server(id, window, cx);
                     cx.notify();
                 }
-                window.focus(&log_view.focus_handle);
+                window.focus(&log_view.focus_handle, cx);
             });
         }
         cx.notify();

crates/language_tools/src/syntax_tree_view.rs πŸ”—

@@ -659,7 +659,7 @@ impl SyntaxTreeToolbarItemView {
             buffer_state.active_layer = Some(layer.to_owned());
             view.selected_descendant_ix = None;
             cx.notify();
-            view.focus_handle.focus(window);
+            view.focus_handle.focus(window, cx);
             Some(())
         })
     }

crates/languages/Cargo.toml πŸ”—

@@ -68,6 +68,7 @@ serde_json.workspace = true
 serde_json_lenient.workspace = true
 settings.workspace = true
 smallvec.workspace = true
+semver.workspace = true
 smol.workspace = true
 snippet.workspace = true
 task.workspace = true

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

@@ -5,6 +5,7 @@ use language::{LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
 use lsp::{LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::lsp_store::language_server_settings;
+use semver::Version;
 use serde_json::json;
 use std::{
     ffi::OsString,
@@ -32,14 +33,14 @@ impl CssLspAdapter {
 }
 
 impl LspInstaller for CssLspAdapter {
-    type BinaryVersion = String;
+    type BinaryVersion = Version;
 
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
         _: bool,
         _: &mut AsyncApp,
-    ) -> Result<String> {
+    ) -> Result<Self::BinaryVersion> {
         self.node
             .npm_package_latest_version("vscode-langservers-extracted")
             .await
@@ -65,11 +66,12 @@ impl LspInstaller for CssLspAdapter {
 
     async fn fetch_server_binary(
         &self,
-        latest_version: String,
+        latest_version: Self::BinaryVersion,
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let server_path = container_dir.join(SERVER_PATH);
+        let latest_version = latest_version.to_string();
 
         self.node
             .npm_install_packages(
@@ -87,7 +89,7 @@ impl LspInstaller for CssLspAdapter {
 
     async fn check_if_version_installed(
         &self,
-        version: &String,
+        version: &Self::BinaryVersion,
         container_dir: &PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {

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

@@ -13,6 +13,7 @@ use language::{
 use lsp::{LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::lsp_store::language_server_settings;
+use semver::Version;
 use serde_json::{Value, json};
 use smol::{
     fs::{self},
@@ -142,14 +143,14 @@ impl JsonLspAdapter {
 }
 
 impl LspInstaller for JsonLspAdapter {
-    type BinaryVersion = String;
+    type BinaryVersion = Version;
 
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
         _: bool,
         _: &mut AsyncApp,
-    ) -> Result<String> {
+    ) -> Result<Self::BinaryVersion> {
         self.node
             .npm_package_latest_version(Self::PACKAGE_NAME)
             .await
@@ -175,7 +176,7 @@ impl LspInstaller for JsonLspAdapter {
 
     async fn check_if_version_installed(
         &self,
-        version: &String,
+        version: &Self::BinaryVersion,
         container_dir: &PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
@@ -204,11 +205,12 @@ impl LspInstaller for JsonLspAdapter {
 
     async fn fetch_server_binary(
         &self,
-        latest_version: String,
+        latest_version: Self::BinaryVersion,
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let server_path = container_dir.join(SERVER_PATH);
+        let latest_version = latest_version.to_string();
 
         self.node
             .npm_install_packages(

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

@@ -19,6 +19,7 @@ use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind};
 use pet_virtualenv::is_virtualenv_dir;
 use project::Fs;
 use project::lsp_store::language_server_settings;
+use semver::Version;
 use serde::{Deserialize, Serialize};
 use serde_json::{Value, json};
 use settings::Settings;
@@ -280,7 +281,7 @@ impl LspInstaller for TyLspAdapter {
         _: &mut AsyncApp,
     ) -> Result<Self::BinaryVersion> {
         let release =
-            latest_github_release("astral-sh/ty", true, true, delegate.http_client()).await?;
+            latest_github_release("astral-sh/ty", true, false, delegate.http_client()).await?;
         let (_, asset_name) = Self::build_asset_name()?;
         let asset = release
             .assets
@@ -294,6 +295,23 @@ impl LspInstaller for TyLspAdapter {
         })
     }
 
+    async fn check_if_user_installed(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+        _: Option<Toolchain>,
+        _: &AsyncApp,
+    ) -> Option<LanguageServerBinary> {
+        let Some(ty_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await else {
+            return None;
+        };
+        let env = delegate.shell_env().await;
+        Some(LanguageServerBinary {
+            path: ty_bin,
+            env: Some(env),
+            arguments: vec!["server".into()],
+        })
+    }
+
     async fn fetch_server_binary(
         &self,
         latest_version: Self::BinaryVersion,
@@ -621,14 +639,14 @@ impl LspAdapter for PyrightLspAdapter {
 }
 
 impl LspInstaller for PyrightLspAdapter {
-    type BinaryVersion = String;
+    type BinaryVersion = Version;
 
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
         _: bool,
         _: &mut AsyncApp,
-    ) -> Result<String> {
+    ) -> Result<Self::BinaryVersion> {
         self.node
             .npm_package_latest_version(Self::SERVER_NAME.as_ref())
             .await
@@ -672,6 +690,7 @@ impl LspInstaller for PyrightLspAdapter {
         delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let server_path = container_dir.join(Self::SERVER_PATH);
+        let latest_version = latest_version.to_string();
 
         self.node
             .npm_install_packages(
@@ -2040,14 +2059,14 @@ impl LspAdapter for BasedPyrightLspAdapter {
 }
 
 impl LspInstaller for BasedPyrightLspAdapter {
-    type BinaryVersion = String;
+    type BinaryVersion = Version;
 
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
         _: bool,
         _: &mut AsyncApp,
-    ) -> Result<String> {
+    ) -> Result<Self::BinaryVersion> {
         self.node
             .npm_package_latest_version(Self::SERVER_NAME.as_ref())
             .await
@@ -2092,6 +2111,7 @@ impl LspInstaller for BasedPyrightLspAdapter {
         delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let server_path = container_dir.join(Self::SERVER_PATH);
+        let latest_version = latest_version.to_string();
 
         self.node
             .npm_install_packages(

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

@@ -355,7 +355,7 @@ impl LspAdapter for RustLspAdapter {
                         | lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }),
                     ) = completion.text_edit.as_ref()
                     && let Ok(mut snippet) = snippet::Snippet::parse(new_text)
-                    && !snippet.tabstops.is_empty()
+                    && snippet.tabstops.len() > 1
                 {
                     label = String::new();
 
@@ -421,7 +421,9 @@ impl LspAdapter for RustLspAdapter {
                             0..label.rfind('(').unwrap_or(completion.label.len()),
                             highlight_id,
                         ));
-                    } else if detail_left.is_none() {
+                    } else if detail_left.is_none()
+                        && kind != Some(lsp::CompletionItemKind::SNIPPET)
+                    {
                         return None;
                     }
                 }
@@ -1597,6 +1599,40 @@ mod tests {
             ))
         );
 
+        // Postfix completion without actual tabstops (only implicit final $0)
+        // The label should use completion.label so it can be filtered by "ref"
+        let ref_completion = adapter
+            .label_for_completion(
+                &lsp::CompletionItem {
+                    kind: Some(lsp::CompletionItemKind::SNIPPET),
+                    label: "ref".to_string(),
+                    filter_text: Some("ref".to_string()),
+                    label_details: Some(CompletionItemLabelDetails {
+                        detail: None,
+                        description: Some("&expr".to_string()),
+                    }),
+                    detail: Some("&expr".to_string()),
+                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                        range: lsp::Range::default(),
+                        new_text: "&String::new()".to_string(),
+                    })),
+                    ..Default::default()
+                },
+                &language,
+            )
+            .await;
+        assert!(
+            ref_completion.is_some(),
+            "ref postfix completion should have a label"
+        );
+        let ref_label = ref_completion.unwrap();
+        let filter_text = &ref_label.text[ref_label.filter_range.clone()];
+        assert!(
+            filter_text.contains("ref"),
+            "filter range text '{filter_text}' should contain 'ref' for filtering to work",
+        );
+
         // Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
         let res = adapter
             .label_for_completion(

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

@@ -6,6 +6,7 @@ use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolc
 use lsp::{LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::lsp_store::language_server_settings;
+use semver::Version;
 use serde_json::{Value, json};
 use std::{
     ffi::OsString,
@@ -39,14 +40,14 @@ impl TailwindLspAdapter {
 }
 
 impl LspInstaller for TailwindLspAdapter {
-    type BinaryVersion = String;
+    type BinaryVersion = Version;
 
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
         _: bool,
         _: &mut AsyncApp,
-    ) -> Result<String> {
+    ) -> Result<Self::BinaryVersion> {
         self.node
             .npm_package_latest_version(Self::PACKAGE_NAME)
             .await
@@ -70,11 +71,12 @@ impl LspInstaller for TailwindLspAdapter {
 
     async fn fetch_server_binary(
         &self,
-        latest_version: String,
+        latest_version: Self::BinaryVersion,
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let server_path = container_dir.join(SERVER_PATH);
+        let latest_version = latest_version.to_string();
 
         self.node
             .npm_install_packages(
@@ -92,7 +94,7 @@ impl LspInstaller for TailwindLspAdapter {
 
     async fn check_if_version_installed(
         &self,
-        version: &String,
+        version: &Self::BinaryVersion,
         container_dir: &PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {

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

@@ -12,6 +12,7 @@ use language::{
 use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::{Fs, lsp_store::language_server_settings};
+use semver::Version;
 use serde_json::{Value, json};
 use smol::lock::RwLock;
 use std::{
@@ -635,8 +636,8 @@ impl TypeScriptLspAdapter {
 }
 
 pub struct TypeScriptVersions {
-    typescript_version: String,
-    server_version: String,
+    typescript_version: Version,
+    server_version: Version,
 }
 
 impl LspInstaller for TypeScriptLspAdapter {
@@ -647,7 +648,7 @@ impl LspInstaller for TypeScriptLspAdapter {
         _: &dyn LspAdapterDelegate,
         _: bool,
         _: &mut AsyncApp,
-    ) -> Result<TypeScriptVersions> {
+    ) -> Result<Self::BinaryVersion> {
         Ok(TypeScriptVersions {
             typescript_version: self
                 .node
@@ -662,7 +663,7 @@ impl LspInstaller for TypeScriptLspAdapter {
 
     async fn check_if_version_installed(
         &self,
-        version: &TypeScriptVersions,
+        version: &Self::BinaryVersion,
         container_dir: &PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
@@ -674,7 +675,7 @@ impl LspInstaller for TypeScriptLspAdapter {
                 Self::PACKAGE_NAME,
                 &server_path,
                 container_dir,
-                VersionStrategy::Latest(version.typescript_version.as_str()),
+                VersionStrategy::Latest(&version.typescript_version),
             )
             .await
         {
@@ -687,7 +688,7 @@ impl LspInstaller for TypeScriptLspAdapter {
                 Self::SERVER_PACKAGE_NAME,
                 &server_path,
                 container_dir,
-                VersionStrategy::Latest(version.server_version.as_str()),
+                VersionStrategy::Latest(&version.server_version),
             )
             .await
         {
@@ -703,7 +704,7 @@ impl LspInstaller for TypeScriptLspAdapter {
 
     async fn fetch_server_binary(
         &self,
-        latest_version: TypeScriptVersions,
+        latest_version: Self::BinaryVersion,
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
@@ -715,11 +716,11 @@ impl LspInstaller for TypeScriptLspAdapter {
                 &[
                     (
                         Self::PACKAGE_NAME,
-                        latest_version.typescript_version.as_str(),
+                        &latest_version.typescript_version.to_string(),
                     ),
                     (
                         Self::SERVER_PACKAGE_NAME,
-                        latest_version.server_version.as_str(),
+                        &latest_version.server_version.to_string(),
                     ),
                 ],
             )

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

@@ -2,12 +2,17 @@ use anyhow::Result;
 use async_trait::async_trait;
 use collections::HashMap;
 use gpui::AsyncApp;
-use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
+use language::{
+    LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, PromptResponseContext, Toolchain,
+};
 use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::{Fs, lsp_store::language_server_settings};
 use regex::Regex;
+use semver::Version;
 use serde_json::Value;
+use serde_json::json;
+use settings::update_settings_file;
 use std::{
     ffi::OsString,
     path::{Path, PathBuf},
@@ -15,6 +20,11 @@ use std::{
 };
 use util::{ResultExt, maybe, merge_json_value_into};
 
+const ACTION_ALWAYS: &str = "Always";
+const ACTION_NEVER: &str = "Never";
+const UPDATE_IMPORTS_MESSAGE_PATTERN: &str = "Update imports for";
+const VTSLS_SERVER_NAME: &str = "vtsls";
+
 fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![server_path.into(), "--stdio".into()]
 }
@@ -74,8 +84,8 @@ impl VtslsLspAdapter {
 }
 
 pub struct TypeScriptVersions {
-    typescript_version: String,
-    server_version: String,
+    typescript_version: Version,
+    server_version: Version,
 }
 
 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls");
@@ -88,7 +98,7 @@ impl LspInstaller for VtslsLspAdapter {
         _: &dyn LspAdapterDelegate,
         _: bool,
         _: &mut AsyncApp,
-    ) -> Result<TypeScriptVersions> {
+    ) -> Result<Self::BinaryVersion> {
         Ok(TypeScriptVersions {
             typescript_version: self.node.npm_package_latest_version("typescript").await?,
             server_version: self
@@ -115,12 +125,15 @@ impl LspInstaller for VtslsLspAdapter {
 
     async fn fetch_server_binary(
         &self,
-        latest_version: TypeScriptVersions,
+        latest_version: Self::BinaryVersion,
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let server_path = container_dir.join(Self::SERVER_PATH);
 
+        let typescript_version = latest_version.typescript_version.to_string();
+        let server_version = latest_version.server_version.to_string();
+
         let mut packages_to_install = Vec::new();
 
         if self
@@ -133,7 +146,7 @@ impl LspInstaller for VtslsLspAdapter {
             )
             .await
         {
-            packages_to_install.push((Self::PACKAGE_NAME, latest_version.server_version.as_str()));
+            packages_to_install.push((Self::PACKAGE_NAME, server_version.as_str()));
         }
 
         if self
@@ -146,10 +159,7 @@ impl LspInstaller for VtslsLspAdapter {
             )
             .await
         {
-            packages_to_install.push((
-                Self::TYPESCRIPT_PACKAGE_NAME,
-                latest_version.typescript_version.as_str(),
-            ));
+            packages_to_install.push((Self::TYPESCRIPT_PACKAGE_NAME, typescript_version.as_str()));
         }
 
         self.node
@@ -301,6 +311,52 @@ impl LspAdapter for VtslsLspAdapter {
             (LanguageName::new_static("TSX"), "typescriptreact".into()),
         ])
     }
+
+    fn process_prompt_response(&self, context: &PromptResponseContext, cx: &mut AsyncApp) {
+        let selected_title = context.selected_action.title.as_str();
+        let is_preference_response =
+            selected_title == ACTION_ALWAYS || selected_title == ACTION_NEVER;
+        if !is_preference_response {
+            return;
+        }
+
+        if context.message.contains(UPDATE_IMPORTS_MESSAGE_PATTERN) {
+            let setting_value = match selected_title {
+                ACTION_ALWAYS => "always",
+                ACTION_NEVER => "never",
+                _ => return,
+            };
+
+            let settings = json!({
+                "typescript": {
+                    "updateImportsOnFileMove": {
+                        "enabled": setting_value
+                    }
+                },
+                "javascript": {
+                    "updateImportsOnFileMove": {
+                        "enabled": setting_value
+                    }
+                }
+            });
+
+            let _ = cx.update(|cx| {
+                update_settings_file(self.fs.clone(), cx, move |content, _| {
+                    let lsp_settings = content
+                        .project
+                        .lsp
+                        .entry(VTSLS_SERVER_NAME.into())
+                        .or_default();
+
+                    if let Some(existing) = &mut lsp_settings.settings {
+                        merge_json_value_into(settings, existing);
+                    } else {
+                        lsp_settings.settings = Some(settings);
+                    }
+                });
+            });
+        }
+    }
 }
 
 async fn get_cached_ts_server_binary(

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

@@ -7,6 +7,7 @@ use language::{
 use lsp::{LanguageServerBinary, LanguageServerName, Uri};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use project::lsp_store::language_server_settings;
+use semver::Version;
 use serde_json::Value;
 use settings::{Settings, SettingsLocation};
 use std::{
@@ -35,14 +36,14 @@ impl YamlLspAdapter {
 }
 
 impl LspInstaller for YamlLspAdapter {
-    type BinaryVersion = String;
+    type BinaryVersion = Version;
 
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
         _: bool,
         _: &mut AsyncApp,
-    ) -> Result<String> {
+    ) -> Result<Self::BinaryVersion> {
         self.node
             .npm_package_latest_version("yaml-language-server")
             .await
@@ -66,7 +67,7 @@ impl LspInstaller for YamlLspAdapter {
 
     async fn fetch_server_binary(
         &self,
-        latest_version: String,
+        latest_version: Self::BinaryVersion,
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
@@ -75,7 +76,7 @@ impl LspInstaller for YamlLspAdapter {
         self.node
             .npm_install_packages(
                 &container_dir,
-                &[(Self::PACKAGE_NAME, latest_version.as_str())],
+                &[(Self::PACKAGE_NAME, &latest_version.to_string())],
             )
             .await?;
 
@@ -88,7 +89,7 @@ impl LspInstaller for YamlLspAdapter {
 
     async fn check_if_version_installed(
         &self,
-        version: &String,
+        version: &Self::BinaryVersion,
         container_dir: &PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {

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

@@ -882,7 +882,9 @@ impl LanguageServer {
                 window: Some(WindowClientCapabilities {
                     work_done_progress: Some(true),
                     show_message: Some(ShowMessageRequestClientCapabilities {
-                        message_action_item: None,
+                        message_action_item: Some(MessageActionItemCapabilities {
+                            additional_properties_support: Some(true),
+                        }),
                     }),
                     ..WindowClientCapabilities::default()
                 }),

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

@@ -151,6 +151,8 @@ actions!(
     [
         /// Copies the selected text to the clipboard.
         Copy,
+        /// Copies the selected text as markdown to the clipboard.
+        CopyAsMarkdown
     ]
 );
 
@@ -295,6 +297,14 @@ impl Markdown {
         cx.write_to_clipboard(ClipboardItem::new_string(text));
     }
 
+    fn copy_as_markdown(&self, _: &mut Window, cx: &mut Context<Self>) {
+        if self.selection.end <= self.selection.start {
+            return;
+        }
+        let text = self.source[self.selection.start..self.selection.end].to_string();
+        cx.write_to_clipboard(ClipboardItem::new_string(text));
+    }
+
     fn parse(&mut self, cx: &mut Context<Self>) {
         if self.source.is_empty() {
             return;
@@ -697,7 +707,7 @@ impl MarkdownElement {
                                 pending: true,
                                 mode,
                             };
-                            window.focus(&markdown.focus_handle);
+                            window.focus(&markdown.focus_handle, cx);
                         }
 
                         window.prevent_default();
@@ -1356,6 +1366,14 @@ impl Element for MarkdownElement {
                 }
             }
         });
+        window.on_action(std::any::TypeId::of::<crate::CopyAsMarkdown>(), {
+            let entity = self.markdown.clone();
+            move |_, phase, window, cx| {
+                if phase == DispatchPhase::Bubble {
+                    entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
+                }
+            }
+        });
 
         self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
         rendered_markdown.element.paint(window, cx);
@@ -1919,7 +1937,7 @@ impl RenderedText {
     }
 
     fn text_for_range(&self, range: Range<usize>) -> String {
-        let mut ret = vec![];
+        let mut accumulator = String::new();
 
         for line in self.lines.iter() {
             if range.start > line.source_end {
@@ -1944,9 +1962,12 @@ impl RenderedText {
             }
             .min(text.len());
 
-            ret.push(text[start..end].to_string());
+            accumulator.push_str(&text[start..end]);
+            accumulator.push('\n');
         }
-        ret.join("\n")
+        // Remove trailing newline
+        accumulator.pop();
+        accumulator
     }
 
     fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {

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

@@ -96,7 +96,7 @@ impl MarkdownPreviewView {
                         pane.add_item(Box::new(view.clone()), false, false, None, window, cx)
                     }
                 });
-                editor.focus_handle(cx).focus(window);
+                editor.focus_handle(cx).focus(window, cx);
                 cx.notify();
             }
         });
@@ -370,7 +370,7 @@ impl MarkdownPreviewView {
                     cx,
                     |selections| selections.select_ranges(vec![selection]),
                 );
-                window.focus(&editor.focus_handle(cx));
+                window.focus(&editor.focus_handle(cx), cx);
             });
         }
     }

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

@@ -155,15 +155,15 @@ impl Model {
     pub fn max_token_count(&self) -> u64 {
         match self {
             Self::CodestralLatest => 256000,
-            Self::MistralLargeLatest => 131000,
+            Self::MistralLargeLatest => 256000,
             Self::MistralMediumLatest => 128000,
             Self::MistralSmallLatest => 32000,
-            Self::MagistralMediumLatest => 40000,
-            Self::MagistralSmallLatest => 40000,
+            Self::MagistralMediumLatest => 128000,
+            Self::MagistralSmallLatest => 128000,
             Self::OpenMistralNemo => 131000,
             Self::OpenCodestralMamba => 256000,
-            Self::DevstralMediumLatest => 128000,
-            Self::DevstralSmallLatest => 262144,
+            Self::DevstralMediumLatest => 256000,
+            Self::DevstralSmallLatest => 256000,
             Self::Pixtral12BLatest => 128000,
             Self::PixtralLargeLatest => 128000,
             Self::Custom { max_tokens, .. } => *max_tokens,

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

@@ -2610,9 +2610,8 @@ impl MultiBuffer {
         for range in ranges {
             let range = range.to_point(&snapshot);
             let start = snapshot.point_to_offset(Point::new(range.start.row, 0));
-            let end = snapshot.point_to_offset(Point::new(range.end.row + 1, 0));
-            let start = start.saturating_sub_usize(1);
-            let end = snapshot.len().min(end + 1usize);
+            let end = (snapshot.point_to_offset(Point::new(range.end.row + 1, 0)) + 1usize)
+                .min(snapshot.len());
             cursor.seek(&start, Bias::Right);
             while let Some(item) = cursor.item() {
                 if *cursor.start() >= end {

crates/multi_buffer/src/multi_buffer_tests.rs πŸ”—

@@ -4480,6 +4480,19 @@ async fn test_word_diff_simple_replacement(cx: &mut TestAppContext) {
     assert_eq!(word_diffs, vec!["world", "bar", "WORLD", "BAR"]);
 }
 
+#[gpui::test]
+async fn test_word_diff_white_space(cx: &mut TestAppContext) {
+    let settings_store = cx.update(|cx| SettingsStore::test(cx));
+    cx.set_global(settings_store);
+
+    let base_text = "hello world foo bar\n";
+    let modified_text = "    hello world foo bar\n";
+
+    let word_diffs = collect_word_diffs(base_text, modified_text, cx);
+
+    assert_eq!(word_diffs, vec!["    "]);
+}
+
 #[gpui::test]
 async fn test_word_diff_consecutive_modified_lines(cx: &mut TestAppContext) {
     let settings_store = cx.update(|cx| SettingsStore::test(cx));

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

@@ -9,8 +9,6 @@ use serde::Deserialize;
 use smol::io::BufReader;
 use smol::{fs, lock::Mutex};
 use std::fmt::Display;
-use std::future::Future;
-use std::pin::Pin;
 use std::{
     env::{self, consts},
     ffi::OsString,
@@ -34,9 +32,9 @@ pub struct NodeBinaryOptions {
 
 pub enum VersionStrategy<'a> {
     /// Install if current version doesn't match pinned version
-    Pin(&'a str),
+    Pin(&'a Version),
     /// Install if current version is older than latest version
-    Latest(&'a str),
+    Latest(&'a Version),
 }
 
 #[derive(Clone)]
@@ -48,7 +46,6 @@ struct NodeRuntimeState {
     last_options: Option<NodeBinaryOptions>,
     options: watch::Receiver<Option<NodeBinaryOptions>>,
     shell_env_loaded: Shared<oneshot::Receiver<()>>,
-    trust_task: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
 }
 
 impl NodeRuntime {
@@ -56,11 +53,9 @@ impl NodeRuntime {
         http: Arc<dyn HttpClient>,
         shell_env_loaded: Option<oneshot::Receiver<()>>,
         options: watch::Receiver<Option<NodeBinaryOptions>>,
-        trust_task: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
     ) -> Self {
         NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState {
             http,
-            trust_task,
             instance: None,
             last_options: None,
             options,
@@ -75,15 +70,11 @@ impl NodeRuntime {
             last_options: None,
             options: watch::channel(Some(NodeBinaryOptions::default())).1,
             shell_env_loaded: oneshot::channel().1.shared(),
-            trust_task: None,
         })))
     }
 
     async fn instance(&self) -> Box<dyn NodeRuntimeTrait> {
         let mut state = self.0.lock().await;
-        if let Some(trust_task) = state.trust_task.take() {
-            trust_task.await;
-        }
 
         let options = loop {
             if let Some(options) = state.options.borrow().as_ref() {
@@ -230,14 +221,14 @@ impl NodeRuntime {
         &self,
         local_package_directory: &Path,
         name: &str,
-    ) -> Result<Option<String>> {
+    ) -> Result<Option<Version>> {
         self.instance()
             .await
             .npm_package_installed_version(local_package_directory, name)
             .await
     }
 
-    pub async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
+    pub async fn npm_package_latest_version(&self, name: &str) -> Result<Version> {
         let http = self.0.lock().await.http.clone();
         let output = self
             .instance()
@@ -280,16 +271,19 @@ impl NodeRuntime {
             .map(|(name, version)| format!("{name}@{version}"))
             .collect();
 
-        let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect();
-        arguments.extend_from_slice(&[
-            "--save-exact",
-            "--fetch-retry-mintimeout",
-            "2000",
-            "--fetch-retry-maxtimeout",
-            "5000",
-            "--fetch-timeout",
-            "5000",
-        ]);
+        let arguments: Vec<_> = packages
+            .iter()
+            .map(|p| p.as_str())
+            .chain([
+                "--save-exact",
+                "--fetch-retry-mintimeout",
+                "2000",
+                "--fetch-retry-maxtimeout",
+                "5000",
+                "--fetch-timeout",
+                "5000",
+            ])
+            .collect();
 
         // This is also wrong because the directory is wrong.
         self.run_npm_subcommand(Some(directory), "install", &arguments)
@@ -320,23 +314,9 @@ impl NodeRuntime {
             return true;
         };
 
-        let Some(installed_version) = Version::parse(&installed_version).log_err() else {
-            return true;
-        };
-
         match version_strategy {
-            VersionStrategy::Pin(pinned_version) => {
-                let Some(pinned_version) = Version::parse(pinned_version).log_err() else {
-                    return true;
-                };
-                installed_version != pinned_version
-            }
-            VersionStrategy::Latest(latest_version) => {
-                let Some(latest_version) = Version::parse(latest_version).log_err() else {
-                    return true;
-                };
-                installed_version < latest_version
-            }
+            VersionStrategy::Pin(pinned_version) => &installed_version != pinned_version,
+            VersionStrategy::Latest(latest_version) => &installed_version < latest_version,
         }
     }
 }
@@ -351,12 +331,12 @@ enum ArchiveType {
 pub struct NpmInfo {
     #[serde(default)]
     dist_tags: NpmInfoDistTags,
-    versions: Vec<String>,
+    versions: Vec<Version>,
 }
 
 #[derive(Debug, Deserialize, Default)]
 pub struct NpmInfoDistTags {
-    latest: Option<String>,
+    latest: Option<Version>,
 }
 
 #[async_trait::async_trait]
@@ -376,7 +356,7 @@ trait NodeRuntimeTrait: Send + Sync {
         &self,
         local_package_directory: &Path,
         name: &str,
-    ) -> Result<Option<String>>;
+    ) -> Result<Option<Version>>;
 }
 
 #[derive(Clone)]
@@ -610,7 +590,7 @@ impl NodeRuntimeTrait for ManagedNodeRuntime {
         &self,
         local_package_directory: &Path,
         name: &str,
-    ) -> Result<Option<String>> {
+    ) -> Result<Option<Version>> {
         read_package_installed_version(local_package_directory.join("node_modules"), name).await
     }
 }
@@ -735,7 +715,7 @@ impl NodeRuntimeTrait for SystemNodeRuntime {
         &self,
         local_package_directory: &Path,
         name: &str,
-    ) -> Result<Option<String>> {
+    ) -> Result<Option<Version>> {
         read_package_installed_version(local_package_directory.join("node_modules"), name).await
         // todo: allow returning a globally installed version (requires callers not to hard-code the path)
     }
@@ -744,7 +724,7 @@ impl NodeRuntimeTrait for SystemNodeRuntime {
 pub async fn read_package_installed_version(
     node_module_directory: PathBuf,
     name: &str,
-) -> Result<Option<String>> {
+) -> Result<Option<Version>> {
     let package_json_path = node_module_directory.join(name).join("package.json");
 
     let mut file = match fs::File::open(package_json_path).await {
@@ -760,7 +740,7 @@ pub async fn read_package_installed_version(
 
     #[derive(Deserialize)]
     struct PackageJson {
-        version: String,
+        version: Version,
     }
 
     let mut contents = String::new();
@@ -797,7 +777,7 @@ impl NodeRuntimeTrait for UnavailableNodeRuntime {
         &self,
         _local_package_directory: &Path,
         _: &str,
-    ) -> Result<Option<String>> {
+    ) -> Result<Option<Version>> {
         bail!("{}", self.error_message)
     }
 }

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

@@ -190,7 +190,7 @@ pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyh
                 let onboarding_page = Onboarding::new(workspace, cx);
                 workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
 
-                window.focus(&onboarding_page.focus_handle(cx));
+                window.focus(&onboarding_page.focus_handle(cx), cx);
 
                 cx.notify();
             };
@@ -277,11 +277,11 @@ impl Render for Onboarding {
             .on_action(Self::handle_sign_in)
             .on_action(Self::handle_open_account)
             .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| {
-                window.focus_next();
+                window.focus_next(cx);
                 cx.notify();
             }))
             .on_action(cx.listener(|_, _: &menu::SelectPrevious, window, cx| {
-                window.focus_prev();
+                window.focus_prev(cx);
                 cx.notify();
             }))
             .child(

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

@@ -311,7 +311,7 @@ impl PickerDelegate for OutlineViewDelegate {
                     |s| s.select_ranges([rows.start..rows.start]),
                 );
                 active_editor.clear_row_highlights::<OutlineRowHighlights>();
-                window.focus(&active_editor.focus_handle(cx));
+                window.focus(&active_editor.focus_handle(cx), cx);
             }
         });
 

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

@@ -998,9 +998,9 @@ impl OutlinePanel {
 
     fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
         if self.filter_editor.focus_handle(cx).is_focused(window) {
-            self.focus_handle.focus(window);
+            self.focus_handle.focus(window, cx);
         } else {
-            self.filter_editor.focus_handle(cx).focus(window);
+            self.filter_editor.focus_handle(cx).focus(window, cx);
         }
 
         if self.context_menu.is_some() {
@@ -1153,9 +1153,9 @@ impl OutlinePanel {
                 }
 
                 if change_focus {
-                    active_editor.focus_handle(cx).focus(window);
+                    active_editor.focus_handle(cx).focus(window, cx);
                 } else {
-                    self.focus_handle.focus(window);
+                    self.focus_handle.focus(window, cx);
                 }
             }
         }
@@ -1458,7 +1458,7 @@ impl OutlinePanel {
                     Box::new(zed_actions::workspace::CopyRelativePath),
                 )
         });
-        window.focus(&context_menu.focus_handle(cx));
+        window.focus(&context_menu.focus_handle(cx), cx);
         let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| {
             outline_panel.context_menu.take();
             cx.notify();
@@ -4539,7 +4539,7 @@ impl OutlinePanel {
         cx: &mut Context<Self>,
     ) {
         if focus {
-            self.focus_handle.focus(window);
+            self.focus_handle.focus(window, cx);
         }
         let ix = self
             .cached_entries

crates/outline_panel/src/outline_panel_settings.rs πŸ”—

@@ -50,7 +50,13 @@ impl Settings for OutlinePanelSettings {
             dock: panel.dock.unwrap(),
             file_icons: panel.file_icons.unwrap(),
             folder_icons: panel.folder_icons.unwrap(),
-            git_status: panel.git_status.unwrap(),
+            git_status: panel.git_status.unwrap()
+                && content
+                    .git
+                    .unwrap()
+                    .enabled
+                    .unwrap()
+                    .is_git_status_enabled(),
             indent_size: panel.indent_size.unwrap(),
             indent_guides: IndentGuidesSettings {
                 show: panel.indent_guides.unwrap().show.unwrap(),

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

@@ -384,7 +384,7 @@ impl<D: PickerDelegate> Picker<D> {
     }
 
     pub fn focus(&self, window: &mut Window, cx: &mut App) {
-        self.focus_handle(cx).focus(window);
+        self.focus_handle(cx).focus(window, cx);
     }
 
     /// Handles the selecting an index, and passing the change to the delegate.

crates/project/Cargo.toml πŸ”—

@@ -40,7 +40,7 @@ clock.workspace = true
 collections.workspace = true
 context_server.workspace = true
 dap.workspace = true
-db.workspace = true
+encoding_rs.workspace = true
 extension.workspace = true
 fancy-regex.workspace = true
 fs.workspace = true

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

@@ -22,6 +22,7 @@ use rpc::{
     proto::{self, ExternalExtensionAgent},
 };
 use schemars::JsonSchema;
+use semver::Version;
 use serde::{Deserialize, Serialize};
 use settings::{RegisterSetting, SettingsStore};
 use task::{Shell, SpawnInTerminal};
@@ -459,7 +460,7 @@ impl AgentServerStore {
                     .gemini
                     .as_ref()
                     .and_then(|settings| settings.ignore_system_version)
-                    .unwrap_or(false),
+                    .unwrap_or(true),
             }),
         );
         self.external_agents.insert(
@@ -974,11 +975,10 @@ fn get_or_npm_install_builtin_agent(
         }
 
         versions.sort();
-        let newest_version = if let Some((version, file_name)) = versions.last().cloned()
+        let newest_version = if let Some((version, _)) = versions.last().cloned()
             && minimum_version.is_none_or(|minimum_version| version >= minimum_version)
         {
-            versions.pop();
-            Some(file_name)
+            versions.pop()
         } else {
             None
         };
@@ -1004,9 +1004,8 @@ fn get_or_npm_install_builtin_agent(
         })
         .detach();
 
-        let version = if let Some(file_name) = newest_version {
+        let version = if let Some((version, file_name)) = newest_version {
             cx.background_spawn({
-                let file_name = file_name.clone();
                 let dir = dir.clone();
                 let fs = fs.clone();
                 async move {
@@ -1015,7 +1014,7 @@ fn get_or_npm_install_builtin_agent(
                         .await
                         .ok();
                     if let Some(latest_version) = latest_version
-                        && &latest_version != &file_name.to_string_lossy()
+                        && latest_version != version
                     {
                         let download_result = download_latest_version(
                             fs,
@@ -1028,7 +1027,9 @@ fn get_or_npm_install_builtin_agent(
                         if let Some(mut new_version_available) = new_version_available
                             && download_result.is_some()
                         {
-                            new_version_available.send(Some(latest_version)).ok();
+                            new_version_available
+                                .send(Some(latest_version.to_string()))
+                                .ok();
                         }
                     }
                 }
@@ -1047,6 +1048,7 @@ fn get_or_npm_install_builtin_agent(
                 package_name.clone(),
             ))
             .await?
+            .to_string()
             .into()
         };
 
@@ -1093,7 +1095,7 @@ async fn download_latest_version(
     dir: PathBuf,
     node_runtime: NodeRuntime,
     package_name: SharedString,
-) -> Result<String> {
+) -> Result<Version> {
     log::debug!("downloading latest version of {package_name}");
 
     let tmp_dir = tempfile::tempdir_in(&dir)?;
@@ -1109,7 +1111,7 @@ async fn download_latest_version(
 
     fs.rename(
         &tmp_dir.keep(),
-        &dir.join(&version),
+        &dir.join(version.to_string()),
         RenameOptions {
             ignore_if_exists: true,
             overwrite: true,

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

@@ -376,6 +376,8 @@ impl LocalBufferStore {
 
         let text = buffer.as_rope().clone();
         let line_ending = buffer.line_ending();
+        let encoding = buffer.encoding();
+        let has_bom = buffer.has_bom();
         let version = buffer.version();
         let buffer_id = buffer.remote_id();
         let file = buffer.file().cloned();
@@ -387,7 +389,7 @@ impl LocalBufferStore {
         }
 
         let save = worktree.update(cx, |worktree, cx| {
-            worktree.write_file(path, text, line_ending, cx)
+            worktree.write_file(path, text, line_ending, encoding, has_bom, cx)
         });
 
         cx.spawn(async move |this, cx| {
@@ -630,7 +632,11 @@ impl LocalBufferStore {
                         })
                         .await;
                     cx.insert_entity(reservation, |_| {
-                        Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite)
+                        let mut buffer =
+                            Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite);
+                        buffer.set_encoding(loaded.encoding);
+                        buffer.set_has_bom(loaded.has_bom);
+                        buffer
                     })?
                 }
                 Err(error) if is_not_found_error(&error) => cx.new(|cx| {

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

@@ -15,7 +15,6 @@ use util::{ResultExt as _, rel_path::RelPath};
 use crate::{
     Project,
     project_settings::{ContextServerSettings, ProjectSettings},
-    trusted_worktrees::wait_for_workspace_trust,
     worktree_store::WorktreeStore,
 };
 
@@ -333,15 +332,6 @@ impl ContextServerStore {
 
     pub fn start_server(&mut self, server: Arc<ContextServer>, cx: &mut Context<Self>) {
         cx.spawn(async move |this, cx| {
-            let wait_task = this.update(cx, |context_server_store, cx| {
-                context_server_store.project.update(cx, |project, cx| {
-                    let remote_host = project.remote_connection_options(cx);
-                    wait_for_workspace_trust(remote_host, "context servers", cx)
-                })
-            })??;
-            if let Some(wait_task) = wait_task {
-                wait_task.await;
-            }
             let this = this.upgrade().context("Context server store dropped")?;
             let settings = this
                 .update(cx, |this, _| {
@@ -582,15 +572,6 @@ impl ContextServerStore {
     }
 
     async fn maintain_servers(this: WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
-        let wait_task = this.update(cx, |context_server_store, cx| {
-            context_server_store.project.update(cx, |project, cx| {
-                let remote_host = project.remote_connection_options(cx);
-                wait_for_workspace_trust(remote_host, "context servers", cx)
-            })
-        })??;
-        if let Some(wait_task) = wait_task {
-            wait_task.await;
-        }
         let (mut configured_servers, registry, worktree_store) = this.update(cx, |this, _| {
             (
                 this.context_server_settings.clone(),

crates/project/src/debugger/session.rs πŸ”—

@@ -3118,10 +3118,11 @@ async fn get_or_install_companion(node: NodeRuntime, cx: &mut AsyncApp) -> Resul
             .await
             .context("getting installed companion version")?
             .context("companion was not installed")?;
-        smol::fs::rename(temp_dir.path(), dir.join(&version))
+        let version_folder = dir.join(version.to_string());
+        smol::fs::rename(temp_dir.path(), &version_folder)
             .await
             .context("moving companion package into place")?;
-        Ok(dir.join(version))
+        Ok(version_folder)
     }
 
     let dir = paths::debug_adapters_dir().join("js-debug-companion");
@@ -3134,19 +3135,23 @@ async fn get_or_install_companion(node: NodeRuntime, cx: &mut AsyncApp) -> Resul
                     .await
                     .context("creating companion installation directory")?;
 
-                let mut children = smol::fs::read_dir(&dir)
+                let children = smol::fs::read_dir(&dir)
                     .await
                     .context("reading companion installation directory")?
                     .try_collect::<Vec<_>>()
                     .await
                     .context("reading companion installation directory entries")?;
-                children
-                    .sort_by_key(|child| semver::Version::parse(child.file_name().to_str()?).ok());
 
-                let latest_installed_version = children.last().and_then(|child| {
-                    let version = child.file_name().into_string().ok()?;
-                    Some((child.path(), version))
-                });
+                let latest_installed_version = children
+                    .iter()
+                    .filter_map(|child| {
+                        Some((
+                            child.path(),
+                            semver::Version::parse(child.file_name().to_str()?).ok()?,
+                        ))
+                    })
+                    .max_by_key(|(_, version)| version.clone());
+
                 let latest_version = node
                     .npm_package_latest_version(PACKAGE_NAME)
                     .await

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

@@ -5867,6 +5867,11 @@ impl Repository {
         self.pending_ops.edit(edits, ());
         ids
     }
+    pub fn default_remote_url(&self) -> Option<String> {
+        self.remote_upstream_url
+            .clone()
+            .or(self.remote_origin_url.clone())
+    }
 }
 
 fn get_permalink_in_rust_registry_src(

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

@@ -93,6 +93,7 @@ use rpc::{
     AnyProtoClient, ErrorCode, ErrorExt as _,
     proto::{LspRequestId, LspRequestMessage as _},
 };
+use semver::Version;
 use serde::Serialize;
 use serde_json::Value;
 use settings::{Settings, SettingsLocation, SettingsStore};
@@ -127,6 +128,7 @@ use util::{
     ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into,
     paths::{PathStyle, SanitizedPath},
     post_inc,
+    redact::redact_command,
     rel_path::RelPath,
 };
 
@@ -576,9 +578,12 @@ impl LocalLspStore {
                                 },
                             },
                         );
-                        log::error!("Failed to start language server {server_name:?}: {err:?}");
+                        log::error!(
+                            "Failed to start language server {server_name:?}: {}",
+                            redact_command(&format!("{err:?}"))
+                        );
                         if !log.is_empty() {
-                            log::error!("server stderr: {log}");
+                            log::error!("server stderr: {}", redact_command(&log));
                         }
                         None
                     }
@@ -1055,12 +1060,15 @@ impl LocalLspStore {
             .on_request::<lsp::request::ShowMessageRequest, _, _>({
                 let this = lsp_store.clone();
                 let name = name.to_string();
+                let adapter = adapter.clone();
                 move |params, cx| {
                     let this = this.clone();
                     let name = name.to_string();
+                    let adapter = adapter.clone();
                     let mut cx = cx.clone();
                     async move {
                         let actions = params.actions.unwrap_or_default();
+                        let message = params.message.clone();
                         let (tx, rx) = smol::channel::bounded(1);
                         let request = LanguageServerPromptRequest {
                             level: match params.typ {
@@ -1081,6 +1089,14 @@ impl LocalLspStore {
                             .is_ok();
                         if did_update {
                             let response = rx.recv().await.ok();
+                            if let Some(ref selected_action) = response {
+                                let context = language::PromptResponseContext {
+                                    message,
+                                    selected_action: selected_action.clone(),
+                                };
+                                adapter.process_prompt_response(&context, &mut cx)
+                            }
+
                             Ok(response)
                         } else {
                             Ok(None)
@@ -2285,12 +2301,10 @@ impl LocalLspStore {
                     && lsp_action.data.is_some()
                     && (lsp_action.command.is_none() || lsp_action.edit.is_none())
                 {
-                    *lsp_action = Box::new(
-                        lang_server
-                            .request::<lsp::request::CodeActionResolveRequest>(*lsp_action.clone())
-                            .await
-                            .into_response()?,
-                    );
+                    **lsp_action = lang_server
+                        .request::<lsp::request::CodeActionResolveRequest>(*lsp_action.clone())
+                        .await
+                        .into_response()?;
                 }
             }
             LspAction::CodeLens(lens) => {
@@ -3301,8 +3315,10 @@ impl LocalLspStore {
         )
         .await
         .log_err();
-        this.update(cx, |this, _| {
+        this.update(cx, |this, cx| {
             if let Some(transaction) = transaction {
+                cx.emit(LspStoreEvent::WorkspaceEditApplied(transaction.clone()));
+
                 this.as_local_mut()
                     .unwrap()
                     .last_workspace_edits_by_language_server
@@ -3842,6 +3858,7 @@ pub enum LspStoreEvent {
         edits: Vec<(lsp::Range, Snippet)>,
         most_recent_edit: clock::Lamport,
     },
+    WorkspaceEditApplied(ProjectTransaction),
 }
 
 #[derive(Clone, Debug, Serialize)]
@@ -6480,7 +6497,7 @@ impl LspStore {
                 server_id == *completion_server_id,
                 "server_id mismatch, applying completion resolve for {server_id} but completion server id is {completion_server_id}"
             );
-            *lsp_completion = Box::new(resolved_completion);
+            **lsp_completion = resolved_completion;
             *resolved = true;
         }
         Ok(())
@@ -6639,7 +6656,7 @@ impl LspStore {
                 server_id == *completion_server_id,
                 "remote server_id mismatch, applying completion resolve for {server_id} but completion server id is {completion_server_id}"
             );
-            *lsp_completion = Box::new(resolved_lsp_completion);
+            **lsp_completion = resolved_lsp_completion;
             *resolved = true;
         }
 
@@ -13983,7 +14000,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
     async fn npm_package_installed_version(
         &self,
         package_name: &str,
-    ) -> Result<Option<(PathBuf, String)>> {
+    ) -> Result<Option<(PathBuf, Version)>> {
         let local_package_directory = self.worktree_root_path();
         let node_modules_directory = local_package_directory.join("node_modules");
 

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

@@ -37,143 +37,7 @@ impl Domain for ProjectDb {
 
 db::static_connection!(PROJECT_DB, ProjectDb, []);
 
-impl ProjectDb {
-    pub(crate) async fn save_trusted_worktrees(
-        &self,
-        trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
-        trusted_workspaces: HashSet<Option<RemoteHostLocation>>,
-    ) -> anyhow::Result<()> {
-        use anyhow::Context as _;
-        use db::sqlez::statement::Statement;
-        use itertools::Itertools as _;
-
-        PROJECT_DB
-            .clear_trusted_worktrees()
-            .await
-            .context("clearing previous trust state")?;
-
-        let trusted_worktrees = trusted_worktrees
-            .into_iter()
-            .flat_map(|(host, abs_paths)| {
-                abs_paths
-                    .into_iter()
-                    .map(move |abs_path| (Some(abs_path), host.clone()))
-            })
-            .chain(trusted_workspaces.into_iter().map(|host| (None, host)))
-            .collect::<Vec<_>>();
-        let mut first_worktree;
-        let mut last_worktree = 0_usize;
-        for (count, placeholders) in std::iter::once("(?, ?, ?)")
-            .cycle()
-            .take(trusted_worktrees.len())
-            .chunks(MAX_QUERY_PLACEHOLDERS / 3)
-            .into_iter()
-            .map(|chunk| {
-                let mut count = 0;
-                let placeholders = chunk
-                    .inspect(|_| {
-                        count += 1;
-                    })
-                    .join(", ");
-                (count, placeholders)
-            })
-            .collect::<Vec<_>>()
-        {
-            first_worktree = last_worktree;
-            last_worktree = last_worktree + count;
-            let query = format!(
-                r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
-VALUES {placeholders};"#
-            );
-
-            let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
-            self.write(move |conn| {
-                let mut statement = Statement::prepare(conn, query)?;
-                let mut next_index = 1;
-                for (abs_path, host) in trusted_worktrees {
-                    let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
-                    next_index = statement.bind(
-                        &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
-                        next_index,
-                    )?;
-                    next_index = statement.bind(
-                        &host
-                            .as_ref()
-                            .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
-                        next_index,
-                    )?;
-                    next_index = statement.bind(
-                        &host.as_ref().map(|host| host.host_identifier.as_str()),
-                        next_index,
-                    )?;
-                }
-                statement.exec()
-            })
-            .await
-            .context("inserting new trusted state")?;
-        }
-        Ok(())
-    }
-
-    pub(crate) fn fetch_trusted_worktrees(
-        &self,
-        worktree_store: Option<Entity<WorktreeStore>>,
-        host: Option<RemoteHostLocation>,
-        cx: &App,
-    ) -> anyhow::Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
-        let trusted_worktrees = PROJECT_DB.trusted_worktrees()?;
-        Ok(trusted_worktrees
-            .into_iter()
-            .map(|(abs_path, user_name, host_name)| {
-                let db_host = match (user_name, host_name) {
-                    (_, None) => None,
-                    (None, Some(host_name)) => Some(RemoteHostLocation {
-                        user_name: None,
-                        host_identifier: SharedString::new(host_name),
-                    }),
-                    (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
-                        user_name: Some(SharedString::new(user_name)),
-                        host_identifier: SharedString::new(host_name),
-                    }),
-                };
-
-                match abs_path {
-                    Some(abs_path) => {
-                        if db_host != host {
-                            (db_host, PathTrust::AbsPath(abs_path))
-                        } else if let Some(worktree_store) = &worktree_store {
-                            find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
-                                .map(PathTrust::Worktree)
-                                .map(|trusted_worktree| (host.clone(), trusted_worktree))
-                                .unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path)))
-                        } else {
-                            (db_host, PathTrust::AbsPath(abs_path))
-                        }
-                    }
-                    None => (db_host, PathTrust::Workspace),
-                }
-            })
-            .fold(HashMap::default(), |mut acc, (remote_host, path_trust)| {
-                acc.entry(remote_host)
-                    .or_insert_with(HashSet::default)
-                    .insert(path_trust);
-                acc
-            }))
-    }
-
-    query! {
-        fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
-            SELECT absolute_path, user_name, host_name
-            FROM trusted_worktrees
-        }
-    }
-
-    query! {
-        pub async fn clear_trusted_worktrees() -> Result<()> {
-            DELETE FROM trusted_worktrees
-        }
-    }
-}
+impl ProjectDb {}
 
 #[cfg(test)]
 mod tests {
@@ -192,220 +56,5 @@ mod tests {
         trusted_worktrees::{PathTrust, RemoteHostLocation},
     };
 
-    static TEST_LOCK: Mutex<()> = Mutex::new(());
-
-    #[gpui::test]
-    async fn test_save_and_fetch_trusted_worktrees(cx: &mut TestAppContext) {
-        cx.executor().allow_parking();
-        let _guard = TEST_LOCK.lock().await;
-        PROJECT_DB.clear_trusted_worktrees().await.unwrap();
-        cx.update(|cx| {
-            if cx.try_global::<SettingsStore>().is_none() {
-                let settings = SettingsStore::test(cx);
-                cx.set_global(settings);
-            }
-        });
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/"),
-            json!({
-                "project_a": { "main.rs": "" },
-                "project_b": { "lib.rs": "" }
-            }),
-        )
-        .await;
-
-        let project = Project::test(
-            fs,
-            [path!("/project_a").as_ref(), path!("/project_b").as_ref()],
-            cx,
-        )
-        .await;
-        let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
-
-        let mut trusted_paths: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> =
-            HashMap::default();
-        trusted_paths.insert(
-            None,
-            HashSet::from_iter([
-                PathBuf::from(path!("/project_a")),
-                PathBuf::from(path!("/project_b")),
-            ]),
-        );
-
-        PROJECT_DB
-            .save_trusted_worktrees(trusted_paths, HashSet::default())
-            .await
-            .unwrap();
-
-        let fetched = cx.update(|cx| {
-            PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
-        });
-        let fetched = fetched.unwrap();
-
-        let local_trust = fetched.get(&None).expect("should have local host entry");
-        assert_eq!(local_trust.len(), 2);
-        assert!(
-            local_trust
-                .iter()
-                .all(|p| matches!(p, PathTrust::Worktree(_)))
-        );
-
-        let fetched_no_store = cx
-            .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
-            .unwrap();
-        let local_trust_no_store = fetched_no_store
-            .get(&None)
-            .expect("should have local host entry");
-        assert_eq!(local_trust_no_store.len(), 2);
-        assert!(
-            local_trust_no_store
-                .iter()
-                .all(|p| matches!(p, PathTrust::AbsPath(_)))
-        );
-    }
-
-    #[gpui::test]
-    async fn test_save_and_fetch_workspace_trust(cx: &mut TestAppContext) {
-        cx.executor().allow_parking();
-        let _guard = TEST_LOCK.lock().await;
-        PROJECT_DB.clear_trusted_worktrees().await.unwrap();
-        cx.update(|cx| {
-            if cx.try_global::<SettingsStore>().is_none() {
-                let settings = SettingsStore::test(cx);
-                cx.set_global(settings);
-            }
-        });
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/root"), json!({ "main.rs": "" }))
-            .await;
-
-        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
-        let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
-
-        let trusted_workspaces = HashSet::from_iter([None]);
-        PROJECT_DB
-            .save_trusted_worktrees(HashMap::default(), trusted_workspaces)
-            .await
-            .unwrap();
-
-        let fetched = cx.update(|cx| {
-            PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
-        });
-        let fetched = fetched.unwrap();
-
-        let local_trust = fetched.get(&None).expect("should have local host entry");
-        assert!(local_trust.contains(&PathTrust::Workspace));
-
-        let fetched_no_store = cx
-            .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
-            .unwrap();
-        let local_trust_no_store = fetched_no_store
-            .get(&None)
-            .expect("should have local host entry");
-        assert!(local_trust_no_store.contains(&PathTrust::Workspace));
-    }
-
-    #[gpui::test]
-    async fn test_save_and_fetch_remote_host_trust(cx: &mut TestAppContext) {
-        cx.executor().allow_parking();
-        let _guard = TEST_LOCK.lock().await;
-        PROJECT_DB.clear_trusted_worktrees().await.unwrap();
-        cx.update(|cx| {
-            if cx.try_global::<SettingsStore>().is_none() {
-                let settings = SettingsStore::test(cx);
-                cx.set_global(settings);
-            }
-        });
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/root"), json!({ "main.rs": "" }))
-            .await;
-
-        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
-        let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
-
-        let remote_host = Some(RemoteHostLocation {
-            user_name: Some(SharedString::from("testuser")),
-            host_identifier: SharedString::from("remote.example.com"),
-        });
-
-        let mut trusted_paths: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> =
-            HashMap::default();
-        trusted_paths.insert(
-            remote_host.clone(),
-            HashSet::from_iter([PathBuf::from("/home/testuser/project")]),
-        );
-
-        PROJECT_DB
-            .save_trusted_worktrees(trusted_paths, HashSet::default())
-            .await
-            .unwrap();
-
-        let fetched = cx.update(|cx| {
-            PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
-        });
-        let fetched = fetched.unwrap();
-
-        let remote_trust = fetched
-            .get(&remote_host)
-            .expect("should have remote host entry");
-        assert_eq!(remote_trust.len(), 1);
-        assert!(remote_trust
-            .iter()
-            .any(|p| matches!(p, PathTrust::AbsPath(path) if path == &PathBuf::from("/home/testuser/project"))));
-
-        let fetched_no_store = cx
-            .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
-            .unwrap();
-        let remote_trust_no_store = fetched_no_store
-            .get(&remote_host)
-            .expect("should have remote host entry");
-        assert_eq!(remote_trust_no_store.len(), 1);
-        assert!(remote_trust_no_store
-            .iter()
-            .any(|p| matches!(p, PathTrust::AbsPath(path) if path == &PathBuf::from("/home/testuser/project"))));
-    }
-
-    #[gpui::test]
-    async fn test_clear_trusted_worktrees(cx: &mut TestAppContext) {
-        cx.executor().allow_parking();
-        let _guard = TEST_LOCK.lock().await;
-        PROJECT_DB.clear_trusted_worktrees().await.unwrap();
-        cx.update(|cx| {
-            if cx.try_global::<SettingsStore>().is_none() {
-                let settings = SettingsStore::test(cx);
-                cx.set_global(settings);
-            }
-        });
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/root"), json!({ "main.rs": "" }))
-            .await;
-
-        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
-        let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
-
-        let trusted_workspaces = HashSet::from_iter([None]);
-        PROJECT_DB
-            .save_trusted_worktrees(HashMap::default(), trusted_workspaces)
-            .await
-            .unwrap();
-
-        PROJECT_DB.clear_trusted_worktrees().await.unwrap();
-
-        let fetched = cx.update(|cx| {
-            PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
-        });
-        let fetched = fetched.unwrap();
-
-        assert!(fetched.is_empty(), "should be empty after clear");
-
-        let fetched_no_store = cx
-            .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
-            .unwrap();
-        assert!(fetched_no_store.is_empty(), "should be empty after clear");
-    }
+    static TEST_WORKTREE_TRUST_LOCK: Mutex<()> = Mutex::new(());
 }

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

@@ -905,7 +905,7 @@ async fn install_prettier_packages(
                     .with_context(|| {
                         format!("fetching latest npm version for package {returned_package_name}")
                     })?;
-                anyhow::Ok((returned_package_name, latest_version))
+                anyhow::Ok((returned_package_name, latest_version.to_string()))
             }),
     )
     .await

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

@@ -10,7 +10,6 @@ pub mod image_store;
 pub mod lsp_command;
 pub mod lsp_store;
 mod manifest_tree;
-mod persistence;
 pub mod prettier_store;
 mod project_search;
 pub mod project_settings;
@@ -66,6 +65,7 @@ use debugger::{
     dap_store::{DapStore, DapStoreEvent},
     session::Session,
 };
+use encoding_rs;
 pub use environment::ProjectEnvironment;
 #[cfg(test)]
 use futures::future::join_all;
@@ -351,6 +351,7 @@ pub enum Event {
     SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
     ExpandedAllForEntry(WorktreeId, ProjectEntryId),
     EntryRenamed(ProjectTransaction, ProjectPath, PathBuf),
+    WorkspaceEditApplied(ProjectTransaction),
     AgentLocationChanged,
 }
 
@@ -2484,13 +2485,11 @@ impl Project {
         cx: &mut Context<Self>,
     ) -> Result<()> {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            self.worktree_store.update(cx, |worktree_store, cx| {
-                for worktree in worktree_store.worktrees() {
-                    store
-                        .clear_local_settings(worktree.read(cx).id(), cx)
-                        .log_err();
-                }
-            });
+            for worktree_metadata in &message.worktrees {
+                store
+                    .clear_local_settings(WorktreeId::from_proto(worktree_metadata.id), cx)
+                    .log_err();
+            }
         });
 
         self.join_project_response_message_id = message_id;
@@ -3252,6 +3251,9 @@ impl Project {
                     cx.emit(Event::SnippetEdit(*buffer_id, edits.clone()))
                 }
             }
+            LspStoreEvent::WorkspaceEditApplied(transaction) => {
+                cx.emit(Event::WorkspaceEditApplied(transaction.clone()))
+            }
         }
     }
 
@@ -4730,6 +4732,14 @@ impl Project {
         this.update(&mut cx, |this, cx| {
             // Don't handle messages that were sent before the response to us joining the project
             if envelope.message_id > this.join_project_response_message_id {
+                cx.update_global::<SettingsStore, _>(|store, cx| {
+                    for worktree_metadata in &envelope.payload.worktrees {
+                        store
+                            .clear_local_settings(WorktreeId::from_proto(worktree_metadata.id), cx)
+                            .log_err();
+                    }
+                });
+
                 this.set_worktrees_from_proto(envelope.payload.worktrees, cx)?;
             }
             Ok(())
@@ -4886,16 +4896,13 @@ impl Project {
             .update(|cx| TrustedWorktrees::try_get_global(cx))?
             .context("missing trusted worktrees")?;
         trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
-            let mut restricted_paths = envelope
+            let restricted_paths = envelope
                 .payload
                 .worktree_ids
                 .into_iter()
                 .map(WorktreeId::from_proto)
                 .map(PathTrust::Worktree)
                 .collect::<HashSet<_>>();
-            if envelope.payload.restrict_workspace {
-                restricted_paths.insert(PathTrust::Workspace);
-            }
             let remote_host = this
                 .read(cx)
                 .remote_connection_options(cx)
@@ -5455,13 +5462,22 @@ impl Project {
                 .await
                 .context("Failed to load settings file")?;
 
+            let has_bom = file.has_bom;
+
             let new_text = cx.read_global::<SettingsStore, _>(|store, cx| {
                 store.new_text_for_update(file.text, move |settings| update(settings, cx))
             })?;
             worktree
                 .update(cx, |worktree, cx| {
                     let line_ending = text::LineEnding::detect(&new_text);
-                    worktree.write_file(rel_path.clone(), new_text.into(), line_ending, cx)
+                    worktree.write_file(
+                        rel_path.clone(),
+                        new_text.into(),
+                        line_ending,
+                        encoding_rs::UTF_8,
+                        has_bom,
+                        cx,
+                    )
                 })?
                 .await
                 .context("Failed to write settings file")?;

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

@@ -332,6 +332,10 @@ impl GoToDiagnosticSeverityFilter {
 
 #[derive(Copy, Clone, Debug)]
 pub struct GitSettings {
+    /// Whether or not git integration is enabled.
+    ///
+    /// Default: true
+    pub enabled: GitEnabledSettings,
     /// Whether or not to show the git gutter.
     ///
     /// Default: tracked_files
@@ -361,6 +365,18 @@ pub struct GitSettings {
     pub path_style: GitPathStyle,
 }
 
+#[derive(Clone, Copy, Debug)]
+pub struct GitEnabledSettings {
+    /// Whether git integration is enabled for showing git status.
+    ///
+    /// Default: true
+    pub status: bool,
+    /// Whether git integration is enabled for showing diffs.
+    ///
+    /// Default: true
+    pub diff: bool,
+}
+
 #[derive(Clone, Copy, Debug, PartialEq, Default)]
 pub enum GitPathStyle {
     #[default]
@@ -502,7 +518,14 @@ impl Settings for ProjectSettings {
         let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
 
         let git = content.git.as_ref().unwrap();
+        let git_enabled = {
+            GitEnabledSettings {
+                status: git.enabled.as_ref().unwrap().is_git_status_enabled(),
+                diff: git.enabled.as_ref().unwrap().is_git_diff_enabled(),
+            }
+        };
         let git_settings = GitSettings {
+            enabled: git_enabled,
             git_gutter: git.git_gutter.unwrap(),
             gutter_debounce: git.gutter_debounce.unwrap_or_default(),
             inline_blame: {

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

@@ -27,36 +27,20 @@
 //! Spawning a language server is potentially dangerous, and Zed needs to restrict that by default.
 //! Each single file worktree requires a separate trust permission, unless a more global level is trusted.
 //!
-//! * "workspace"
-//!
-//! Even an empty Zed instance with no files or directories open is potentially dangerous: opening an Assistant Panel and creating new external agent thread might require installing and running MCP servers.
-//!
-//! Disabling the entire panel is possible with ai-related settings.
-//! Yet when it's enabled, it's still reasonably safe to use remote AI agents and control their permissions in the Assistant Panel.
-//!
-//! Unlike that, MCP servers are similar to language servers and may require fetching, installing and running packages or binaries.
-//! Given that those servers are not tied to any particular worktree, this level of trust is required to operate any MCP server.
-//!
-//! Workspace level of trust assumes all single file worktrees are trusted too, for the same host: if we allow global MCP server-related functionality, we can already allow spawning language servers for single file worktrees as well.
-//!
 //! * "directory worktree"
 //!
 //! If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it.
 //! Each such worktree requires a separate trust permission, so each separate directory worktree has to be trusted separately, unless a more global level is trusted.
 //!
-//! When a directory worktree is trusted and language servers are allowed to be downloaded and started, hence we also allow workspace level of trust (hence, "single file worktree" level of trust also).
+//! When a directory worktree is trusted and language servers are allowed to be downloaded and started, hence, "single file worktree" level of trust also.
 //!
 //! * "path override"
 //!
 //! To ease trusting multiple directory worktrees at once, it's possible to trust a parent directory of a certain directory worktree opened in Zed.
 //! Trusting a directory means trusting all its subdirectories as well, including all current and potential directory worktrees.
-//!
-//! If we trust multiple projects to install and spawn various language server processes, we can also allow workspace trust requests for MCP servers installation and spawning.
 
 use collections::{HashMap, HashSet};
-use gpui::{
-    App, AppContext as _, Context, Entity, EventEmitter, Global, SharedString, Task, WeakEntity,
-};
+use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, SharedString, WeakEntity};
 use remote::RemoteConnectionOptions;
 use rpc::{AnyProtoClient, proto};
 use settings::{Settings as _, WorktreeId};
@@ -68,19 +52,21 @@ use util::debug_panic;
 
 use crate::{project_settings::ProjectSettings, worktree_store::WorktreeStore};
 
-#[cfg(not(any(test, feature = "test-support")))]
-use crate::persistence::PROJECT_DB;
-#[cfg(not(any(test, feature = "test-support")))]
-use util::ResultExt as _;
-
 pub fn init(
+    db_trusted_paths: TrustedPaths,
     downstream_client: Option<(AnyProtoClient, u64)>,
     upstream_client: Option<(AnyProtoClient, u64)>,
     cx: &mut App,
 ) {
     if TrustedWorktrees::try_get_global(cx).is_none() {
-        let trusted_worktrees = cx.new(|cx| {
-            TrustedWorktreesStore::new(None, None, downstream_client, upstream_client, cx)
+        let trusted_worktrees = cx.new(|_| {
+            TrustedWorktreesStore::new(
+                db_trusted_paths,
+                None,
+                None,
+                downstream_client,
+                upstream_client,
+            )
         });
         cx.set_global(TrustedWorktrees(trusted_worktrees))
     }
@@ -126,72 +112,10 @@ pub fn track_worktree_trust(
                 }
             });
         }
-        None => {
-            let trusted_worktrees = cx.new(|cx| {
-                TrustedWorktreesStore::new(
-                    Some(worktree_store.clone()),
-                    remote_host,
-                    downstream_client,
-                    upstream_client,
-                    cx,
-                )
-            });
-            cx.set_global(TrustedWorktrees(trusted_worktrees))
-        }
+        None => log::debug!("No TrustedWorktrees initialized, not tracking worktree trust"),
     }
 }
 
-/// Waits until at least [`PathTrust::Workspace`] level of trust is granted for the host the [`TrustedWorktrees`] was initialized with.
-pub fn wait_for_default_workspace_trust(
-    what_waits: &'static str,
-    cx: &mut App,
-) -> Option<Task<()>> {
-    let trusted_worktrees = TrustedWorktrees::try_get_global(cx)?;
-    wait_for_workspace_trust(
-        trusted_worktrees.read(cx).remote_host.clone(),
-        what_waits,
-        cx,
-    )
-}
-
-/// Waits until at least [`PathTrust::Workspace`] level of trust is granted for a particular host.
-pub fn wait_for_workspace_trust(
-    remote_host: Option<impl Into<RemoteHostLocation>>,
-    what_waits: &'static str,
-    cx: &mut App,
-) -> Option<Task<()>> {
-    let trusted_worktrees = TrustedWorktrees::try_get_global(cx)?;
-    let remote_host = remote_host.map(|host| host.into());
-
-    let remote_host = if trusted_worktrees.update(cx, |trusted_worktrees, cx| {
-        trusted_worktrees.can_trust_workspace(remote_host.clone(), cx)
-    }) {
-        None
-    } else {
-        Some(remote_host)
-    }?;
-
-    Some(cx.spawn(async move |cx| {
-        log::info!("Waiting for workspace to be trusted before starting {what_waits}");
-        let (tx, restricted_worktrees_task) = smol::channel::bounded::<()>(1);
-        let Ok(_subscription) = cx.update(|cx| {
-            cx.subscribe(&trusted_worktrees, move |_, e, _| {
-                if let TrustedWorktreesEvent::Trusted(trusted_host, trusted_paths) = e {
-                    if trusted_host == &remote_host && trusted_paths.contains(&PathTrust::Workspace)
-                    {
-                        log::info!("Workspace is trusted for {what_waits}");
-                        tx.send_blocking(()).ok();
-                    }
-                }
-            })
-        }) else {
-            return;
-        };
-
-        restricted_worktrees_task.recv().await.ok();
-    }))
-}
-
 /// A collection of worktree trust metadata, can be accessed globally (if initialized) and subscribed to.
 pub struct TrustedWorktrees(Entity<TrustedWorktreesStore>);
 
@@ -212,12 +136,8 @@ pub struct TrustedWorktreesStore {
     downstream_client: Option<(AnyProtoClient, u64)>,
     upstream_client: Option<(AnyProtoClient, u64)>,
     worktree_stores: HashMap<WeakEntity<WorktreeStore>, Option<RemoteHostLocation>>,
-    trusted_paths: HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>,
-    #[cfg(not(any(test, feature = "test-support")))]
-    serialization_task: Task<()>,
+    trusted_paths: TrustedPaths,
     restricted: HashSet<WorktreeId>,
-    remote_host: Option<RemoteHostLocation>,
-    restricted_workspaces: HashSet<Option<RemoteHostLocation>>,
 }
 
 /// An identifier of a host to split the trust questions by.
@@ -234,7 +154,7 @@ impl From<RemoteConnectionOptions> for RemoteHostLocation {
         let (user_name, host_name) = match options {
             RemoteConnectionOptions::Ssh(ssh) => (
                 ssh.username.map(SharedString::new),
-                SharedString::new(ssh.host),
+                SharedString::new(ssh.host.to_string()),
             ),
             RemoteConnectionOptions::Wsl(wsl) => (
                 wsl.user.map(SharedString::new),
@@ -257,9 +177,6 @@ impl From<RemoteConnectionOptions> for RemoteHostLocation {
 /// See module-level documentation on the trust model.
 #[derive(Debug, PartialEq, Eq, Clone, Hash)]
 pub enum PathTrust {
-    /// General, no worktrees or files open case.
-    /// E.g. MCP servers can be spawned from a blank Zed instance, but will do `npm i` and other potentially malicious actions.
-    Workspace,
     /// A worktree that is familiar to this workspace.
     /// Either a single file or a directory worktree.
     Worktree(WorktreeId),
@@ -271,9 +188,6 @@ pub enum PathTrust {
 impl PathTrust {
     fn to_proto(&self) -> proto::PathTrust {
         match self {
-            Self::Workspace => proto::PathTrust {
-                content: Some(proto::path_trust::Content::Workspace(0)),
-            },
             Self::Worktree(worktree_id) => proto::PathTrust {
                 content: Some(proto::path_trust::Content::WorktreeId(
                     worktree_id.to_proto(),
@@ -293,7 +207,6 @@ impl PathTrust {
                 Self::Worktree(WorktreeId::from_proto(id))
             }
             proto::path_trust::Content::AbsPath(path) => Self::AbsPath(PathBuf::from(path)),
-            proto::path_trust::Content::Workspace(_) => Self::Workspace,
         })
     }
 }
@@ -307,36 +220,16 @@ pub enum TrustedWorktreesEvent {
 
 impl EventEmitter<TrustedWorktreesEvent> for TrustedWorktreesStore {}
 
+pub type TrustedPaths = HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>;
+
 impl TrustedWorktreesStore {
     fn new(
+        trusted_paths: TrustedPaths,
         worktree_store: Option<Entity<WorktreeStore>>,
         remote_host: Option<RemoteHostLocation>,
         downstream_client: Option<(AnyProtoClient, u64)>,
         upstream_client: Option<(AnyProtoClient, u64)>,
-        cx: &App,
     ) -> Self {
-        #[cfg(any(test, feature = "test-support"))]
-        let _ = cx;
-
-        #[cfg(not(any(test, feature = "test-support")))]
-        let trusted_paths = if downstream_client.is_none() {
-            match PROJECT_DB.fetch_trusted_worktrees(
-                worktree_store.clone(),
-                remote_host.clone(),
-                cx,
-            ) {
-                Ok(trusted_paths) => trusted_paths,
-                Err(e) => {
-                    log::error!("Failed to do initial trusted worktrees fetch: {e:#}");
-                    HashMap::default()
-                }
-            }
-        } else {
-            HashMap::default()
-        };
-        #[cfg(any(test, feature = "test-support"))]
-        let trusted_paths = HashMap::<Option<RemoteHostLocation>, HashSet<PathTrust>>::default();
-
         if let Some((upstream_client, upstream_project_id)) = &upstream_client {
             let trusted_paths = trusted_paths
                 .iter()
@@ -353,9 +246,7 @@ impl TrustedWorktreesStore {
         }
 
         let worktree_stores = match worktree_store {
-            Some(worktree_store) => {
-                HashMap::from_iter([(worktree_store.downgrade(), remote_host.clone())])
-            }
+            Some(worktree_store) => HashMap::from_iter([(worktree_store.downgrade(), remote_host)]),
             None => HashMap::default(),
         };
 
@@ -363,11 +254,7 @@ impl TrustedWorktreesStore {
             trusted_paths,
             downstream_client,
             upstream_client,
-            remote_host,
-            restricted_workspaces: HashSet::default(),
             restricted: HashSet::default(),
-            #[cfg(not(any(test, feature = "test-support")))]
-            serialization_task: Task::ready(()),
             worktree_stores,
         }
     }
@@ -378,11 +265,9 @@ impl TrustedWorktreesStore {
         worktree_store: &Entity<WorktreeStore>,
         cx: &App,
     ) -> bool {
-        let Some(remote_host) = self.worktree_stores.get(&worktree_store.downgrade()) else {
-            return false;
-        };
-        self.restricted_workspaces.contains(remote_host)
-            || self.restricted.iter().any(|restricted_worktree| {
+        self.worktree_stores
+            .contains_key(&worktree_store.downgrade())
+            && self.restricted.iter().any(|restricted_worktree| {
                 worktree_store
                     .read(cx)
                     .worktree_for_id(*restricted_worktree, cx)
@@ -399,7 +284,6 @@ impl TrustedWorktreesStore {
         remote_host: Option<RemoteHostLocation>,
         cx: &mut Context<Self>,
     ) {
-        let mut new_workspace_trusted = false;
         let mut new_trusted_single_file_worktrees = HashSet::default();
         let mut new_trusted_other_worktrees = HashSet::default();
         let mut new_trusted_abs_paths = HashSet::default();
@@ -410,7 +294,6 @@ impl TrustedWorktreesStore {
                 .flat_map(|current_trusted| current_trusted.iter()),
         ) {
             match trusted_path {
-                PathTrust::Workspace => new_workspace_trusted = true,
                 PathTrust::Worktree(worktree_id) => {
                     self.restricted.remove(worktree_id);
                     if let Some((abs_path, is_file, host)) =
@@ -421,13 +304,11 @@ impl TrustedWorktreesStore {
                                 new_trusted_single_file_worktrees.insert(*worktree_id);
                             } else {
                                 new_trusted_other_worktrees.insert((abs_path, *worktree_id));
-                                new_workspace_trusted = true;
                             }
                         }
                     }
                 }
                 PathTrust::AbsPath(path) => {
-                    new_workspace_trusted = true;
                     debug_assert!(
                         path.is_absolute(),
                         "Cannot trust non-absolute path {path:?}"
@@ -437,11 +318,6 @@ impl TrustedWorktreesStore {
             }
         }
 
-        if new_workspace_trusted {
-            new_trusted_single_file_worktrees.clear();
-            self.restricted_workspaces.remove(&remote_host);
-            trusted_paths.insert(PathTrust::Workspace);
-        }
         new_trusted_other_worktrees.retain(|(worktree_abs_path, _)| {
             new_trusted_abs_paths
                 .iter()
@@ -461,8 +337,7 @@ impl TrustedWorktreesStore {
                 if restricted_host != remote_host {
                     return true;
                 }
-                let retain = (!is_file
-                    || (!new_workspace_trusted && new_trusted_other_worktrees.is_empty()))
+                let retain = (!is_file || new_trusted_other_worktrees.is_empty())
                     && new_trusted_abs_paths.iter().all(|new_trusted_path| {
                         !restricted_worktree_path.starts_with(new_trusted_path)
                     });
@@ -486,9 +361,6 @@ impl TrustedWorktreesStore {
                     .into_iter()
                     .map(PathTrust::Worktree),
             );
-            if trusted_paths.is_empty() && new_workspace_trusted {
-                trusted_paths.insert(PathTrust::Workspace);
-            }
         }
 
         cx.emit(TrustedWorktreesEvent::Trusted(
@@ -496,41 +368,6 @@ impl TrustedWorktreesStore {
             trusted_paths.clone(),
         ));
 
-        #[cfg(not(any(test, feature = "test-support")))]
-        if self.downstream_client.is_none() {
-            let mut new_trusted_workspaces = HashSet::default();
-            let new_trusted_worktrees = self
-                .trusted_paths
-                .clone()
-                .into_iter()
-                .map(|(host, paths)| {
-                    let abs_paths = paths
-                        .into_iter()
-                        .flat_map(|path| match path {
-                            PathTrust::Worktree(worktree_id) => self
-                                .find_worktree_data(worktree_id, cx)
-                                .map(|(abs_path, ..)| abs_path.to_path_buf()),
-                            PathTrust::AbsPath(abs_path) => Some(abs_path),
-                            PathTrust::Workspace => {
-                                new_trusted_workspaces.insert(host.clone());
-                                None
-                            }
-                        })
-                        .collect();
-                    (host, abs_paths)
-                })
-                .collect();
-            // Do not persist auto trusted worktrees
-            if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
-                self.serialization_task = cx.background_spawn(async move {
-                    PROJECT_DB
-                        .save_trusted_worktrees(new_trusted_worktrees, new_trusted_workspaces)
-                        .await
-                        .log_err();
-                });
-            }
-        }
-
         if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
             let trusted_paths = trusted_paths
                 .iter()
@@ -557,13 +394,6 @@ impl TrustedWorktreesStore {
     ) {
         for restricted_path in restricted_paths {
             match restricted_path {
-                PathTrust::Workspace => {
-                    self.restricted_workspaces.insert(remote_host.clone());
-                    cx.emit(TrustedWorktreesEvent::Restricted(
-                        remote_host.clone(),
-                        HashSet::from_iter([PathTrust::Workspace]),
-                    ));
-                }
                 PathTrust::Worktree(worktree_id) => {
                     self.restricted.insert(worktree_id);
                     cx.emit(TrustedWorktreesEvent::Restricted(
@@ -578,31 +408,8 @@ impl TrustedWorktreesStore {
 
     /// Erases all trust information.
     /// Requires Zed's restart to take proper effect.
-    pub fn clear_trusted_paths(&mut self, cx: &App) -> Task<()> {
-        if self.downstream_client.is_none() {
-            self.trusted_paths.clear();
-
-            #[cfg(not(any(test, feature = "test-support")))]
-            {
-                let (tx, rx) = smol::channel::bounded(1);
-                self.serialization_task = cx.background_spawn(async move {
-                    PROJECT_DB.clear_trusted_worktrees().await.log_err();
-                    tx.send(()).await.ok();
-                });
-
-                return cx.background_spawn(async move {
-                    rx.recv().await.ok();
-                });
-            }
-
-            #[cfg(any(test, feature = "test-support"))]
-            {
-                let _ = cx;
-                Task::ready(())
-            }
-        } else {
-            Task::ready(())
-        }
+    pub fn clear_trusted_paths(&mut self) {
+        self.trusted_paths.clear();
     }
 
     /// Checks whether a certain worktree is trusted (or on a larger trust level).
@@ -659,7 +466,6 @@ impl TrustedWorktreesStore {
             downstream_client
                 .send(proto::RestrictWorktrees {
                     project_id: *downstream_project_id,
-                    restrict_workspace: false,
                     worktree_ids: vec![worktree_id.to_proto()],
                 })
                 .ok();
@@ -668,7 +474,6 @@ impl TrustedWorktreesStore {
             upstream_client
                 .send(proto::RestrictWorktrees {
                     project_id: *upstream_project_id,
-                    restrict_workspace: false,
                     worktree_ids: vec![worktree_id.to_proto()],
                 })
                 .ok();
@@ -676,61 +481,12 @@ impl TrustedWorktreesStore {
         false
     }
 
-    /// Checks whether a certain worktree is trusted globally (or on a larger trust level).
-    /// If not, emits [`TrustedWorktreesEvent::Restricted`] event if checked for the first time and not trusted.
-    ///
-    /// No events or data adjustment happens when `trust_all_worktrees` auto trust is enabled.
-    pub fn can_trust_workspace(
-        &mut self,
-        remote_host: Option<RemoteHostLocation>,
-        cx: &mut Context<Self>,
-    ) -> bool {
-        if ProjectSettings::get_global(cx).session.trust_all_worktrees {
-            return true;
-        }
-        if self.restricted_workspaces.contains(&remote_host) {
-            return false;
-        }
-        if self.trusted_paths.contains_key(&remote_host) {
-            return true;
-        }
-
-        self.restricted_workspaces.insert(remote_host.clone());
-        cx.emit(TrustedWorktreesEvent::Restricted(
-            remote_host.clone(),
-            HashSet::from_iter([PathTrust::Workspace]),
-        ));
-
-        if remote_host == self.remote_host {
-            if let Some((downstream_client, downstream_project_id)) = &self.downstream_client {
-                downstream_client
-                    .send(proto::RestrictWorktrees {
-                        project_id: *downstream_project_id,
-                        restrict_workspace: true,
-                        worktree_ids: Vec::new(),
-                    })
-                    .ok();
-            }
-            if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
-                upstream_client
-                    .send(proto::RestrictWorktrees {
-                        project_id: *upstream_project_id,
-                        restrict_workspace: true,
-                        worktree_ids: Vec::new(),
-                    })
-                    .ok();
-            }
-        }
-        false
-    }
-
-    /// Lists all explicitly restricted worktrees (via [`TrustedWorktreesStore::can_trust`] and [`TrustedWorktreesStore::can_trust_workspace`] method calls) for a particular worktree store on a particular host.
+    /// Lists all explicitly restricted worktrees (via [`TrustedWorktreesStore::can_trust`] method calls) for a particular worktree store on a particular host.
     pub fn restricted_worktrees(
         &self,
         worktree_store: &WorktreeStore,
-        remote_host: Option<RemoteHostLocation>,
         cx: &App,
-    ) -> HashSet<Option<(WorktreeId, Arc<Path>)>> {
+    ) -> HashSet<(WorktreeId, Arc<Path>)> {
         let mut single_file_paths = HashSet::default();
         let other_paths = self
             .restricted
@@ -740,19 +496,16 @@ impl TrustedWorktreesStore {
                 let worktree = worktree.read(cx);
                 let abs_path = worktree.abs_path();
                 if worktree.is_single_file() {
-                    single_file_paths.insert(Some((restricted_worktree_id, abs_path)));
+                    single_file_paths.insert((restricted_worktree_id, abs_path));
                     None
                 } else {
                     Some((restricted_worktree_id, abs_path))
                 }
             })
-            .map(Some)
             .collect::<HashSet<_>>();
 
         if !other_paths.is_empty() {
             return other_paths;
-        } else if self.restricted_workspaces.contains(&remote_host) {
-            return HashSet::from_iter([None]);
         } else {
             single_file_paths
         }
@@ -761,7 +514,7 @@ impl TrustedWorktreesStore {
     /// Switches the "trust nothing" mode to "automatically trust everything".
     /// This does not influence already persisted data, but stops adding new worktrees there.
     pub fn auto_trust_all(&mut self, cx: &mut Context<Self>) {
-        for (remote_host, mut worktrees) in std::mem::take(&mut self.restricted)
+        for (remote_host, worktrees) in std::mem::take(&mut self.restricted)
             .into_iter()
             .flat_map(|restricted_worktree| {
                 let (_, _, host) = self.find_worktree_data(restricted_worktree, cx)?;
@@ -774,15 +527,33 @@ impl TrustedWorktreesStore {
                 acc
             })
         {
-            if self.restricted_workspaces.remove(&remote_host) {
-                worktrees.insert(PathTrust::Workspace);
-            }
             self.trust(worktrees, remote_host, cx);
         }
+    }
 
-        for remote_host in std::mem::take(&mut self.restricted_workspaces) {
-            self.trust(HashSet::from_iter([PathTrust::Workspace]), remote_host, cx);
-        }
+    /// Returns a normalized representation of the trusted paths to store in the DB.
+    pub fn trusted_paths_for_serialization(
+        &mut self,
+        cx: &mut Context<Self>,
+    ) -> HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> {
+        let new_trusted_worktrees = self
+            .trusted_paths
+            .clone()
+            .into_iter()
+            .map(|(host, paths)| {
+                let abs_paths = paths
+                    .into_iter()
+                    .flat_map(|path| match path {
+                        PathTrust::Worktree(worktree_id) => self
+                            .find_worktree_data(worktree_id, cx)
+                            .map(|(abs_path, ..)| abs_path.to_path_buf()),
+                        PathTrust::AbsPath(abs_path) => Some(abs_path),
+                    })
+                    .collect();
+                (host, abs_paths)
+            })
+            .collect();
+        new_trusted_worktrees
     }
 
     fn find_worktree_data(
@@ -841,7 +612,7 @@ impl TrustedWorktreesStore {
     }
 }
 
-pub(crate) fn find_worktree_in_store(
+pub fn find_worktree_in_store(
     worktree_store: &WorktreeStore,
     abs_path: &Path,
     cx: &App,
@@ -885,6 +656,7 @@ mod tests {
         cx: &mut TestAppContext,
     ) -> Entity<TrustedWorktreesStore> {
         cx.update(|cx| {
+            init(HashMap::default(), None, None, cx);
             track_worktree_trust(worktree_store, None, None, None, cx);
             TrustedWorktrees::try_get_global(cx).expect("global should be set")
         })
@@ -945,15 +717,9 @@ mod tests {
         assert!(has_restricted, "should have restricted worktrees");
 
         let restricted = worktree_store.read_with(cx, |ws, cx| {
-            trusted_worktrees
-                .read(cx)
-                .restricted_worktrees(ws, None, cx)
+            trusted_worktrees.read(cx).restricted_worktrees(ws, cx)
         });
-        assert!(
-            restricted
-                .iter()
-                .any(|r| r.as_ref().map(|(id, _)| *id) == Some(worktree_id))
-        );
+        assert!(restricted.iter().any(|(id, _)| *id == worktree_id));
 
         events.borrow_mut().clear();
 
@@ -998,9 +764,7 @@ mod tests {
         );
 
         let restricted_after = worktree_store.read_with(cx, |ws, cx| {
-            trusted_worktrees
-                .read(cx)
-                .restricted_worktrees(ws, None, cx)
+            trusted_worktrees.read(cx).restricted_worktrees(ws, cx)
         });
         assert!(
             restricted_after.is_empty(),
@@ -1008,92 +772,6 @@ mod tests {
         );
     }
 
-    #[gpui::test]
-    async fn test_workspace_trust_no_worktrees(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/root"), json!({})).await;
-
-        let project = Project::test(fs, Vec::<&Path>::new(), cx).await;
-        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
-
-        let trusted_worktrees = init_trust_global(worktree_store, cx);
-
-        let events: Rc<RefCell<Vec<TrustedWorktreesEvent>>> = Rc::default();
-        cx.update({
-            let events = events.clone();
-            |cx| {
-                cx.subscribe(&trusted_worktrees, move |_, event, _| {
-                    events.borrow_mut().push(match event {
-                        TrustedWorktreesEvent::Trusted(host, paths) => {
-                            TrustedWorktreesEvent::Trusted(host.clone(), paths.clone())
-                        }
-                        TrustedWorktreesEvent::Restricted(host, paths) => {
-                            TrustedWorktreesEvent::Restricted(host.clone(), paths.clone())
-                        }
-                    });
-                })
-            }
-        })
-        .detach();
-
-        let can_trust_workspace =
-            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
-        assert!(
-            !can_trust_workspace,
-            "workspace should be restricted by default"
-        );
-
-        {
-            let events = events.borrow();
-            assert_eq!(events.len(), 1);
-            match &events[0] {
-                TrustedWorktreesEvent::Restricted(host, paths) => {
-                    assert!(host.is_none());
-                    assert!(paths.contains(&PathTrust::Workspace));
-                }
-                _ => panic!("expected Restricted event"),
-            }
-        }
-
-        events.borrow_mut().clear();
-
-        let can_trust_workspace_again =
-            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
-        assert!(
-            !can_trust_workspace_again,
-            "workspace should still be restricted"
-        );
-        assert!(
-            events.borrow().is_empty(),
-            "no duplicate Restricted event on repeated can_trust_workspace"
-        );
-
-        trusted_worktrees.update(cx, |store, cx| {
-            store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
-        });
-
-        {
-            let events = events.borrow();
-            assert_eq!(events.len(), 1);
-            match &events[0] {
-                TrustedWorktreesEvent::Trusted(host, paths) => {
-                    assert!(host.is_none());
-                    assert!(paths.contains(&PathTrust::Workspace));
-                }
-                _ => panic!("expected Trusted event"),
-            }
-        }
-
-        let can_trust_workspace_after =
-            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
-        assert!(
-            can_trust_workspace_after,
-            "workspace should be trusted after trust()"
-        );
-    }
-
     #[gpui::test]
     async fn test_single_file_worktree_trust(cx: &mut TestAppContext) {
         init_test(cx);
@@ -1179,58 +857,6 @@ mod tests {
         );
     }
 
-    #[gpui::test]
-    async fn test_workspace_trust_unlocks_single_file_worktree(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/root"), json!({ "foo.rs": "fn foo() {}" }))
-            .await;
-
-        let project = Project::test(fs, [path!("/root/foo.rs").as_ref()], cx).await;
-        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
-        let worktree_id = worktree_store.read_with(cx, |store, cx| {
-            let worktree = store.worktrees().next().unwrap();
-            let worktree = worktree.read(cx);
-            assert!(worktree.is_single_file(), "expected single-file worktree");
-            worktree.id()
-        });
-
-        let trusted_worktrees = init_trust_global(worktree_store, cx);
-
-        let can_trust_workspace =
-            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
-        assert!(
-            !can_trust_workspace,
-            "workspace should be restricted by default"
-        );
-
-        let can_trust_file =
-            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
-        assert!(
-            !can_trust_file,
-            "single-file worktree should be restricted by default"
-        );
-
-        trusted_worktrees.update(cx, |store, cx| {
-            store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
-        });
-
-        let can_trust_workspace_after =
-            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
-        assert!(
-            can_trust_workspace_after,
-            "workspace should be trusted after trust(Workspace)"
-        );
-
-        let can_trust_file_after =
-            trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
-        assert!(
-            can_trust_file_after,
-            "single-file worktree should be trusted after workspace trust"
-        );
-    }
-
     #[gpui::test]
     async fn test_multiple_single_file_worktrees_trust_one(cx: &mut TestAppContext) {
         init_test(cx);
@@ -1376,47 +1002,6 @@ mod tests {
         assert!(can_trust_b, "project_b should now be trusted");
     }
 
-    #[gpui::test]
-    async fn test_directory_worktree_trust_enables_workspace(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
-            .await;
-
-        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
-        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
-        let worktree_id = worktree_store.read_with(cx, |store, cx| {
-            let worktree = store.worktrees().next().unwrap();
-            assert!(!worktree.read(cx).is_single_file());
-            worktree.read(cx).id()
-        });
-
-        let trusted_worktrees = init_trust_global(worktree_store, cx);
-
-        let can_trust_workspace =
-            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
-        assert!(
-            !can_trust_workspace,
-            "workspace should be restricted initially"
-        );
-
-        trusted_worktrees.update(cx, |store, cx| {
-            store.trust(
-                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
-                None,
-                cx,
-            );
-        });
-
-        let can_trust_workspace_after =
-            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
-        assert!(
-            can_trust_workspace_after,
-            "workspace should be trusted after trusting directory worktree"
-        );
-    }
-
     #[gpui::test]
     async fn test_directory_worktree_trust_enables_single_file(cx: &mut TestAppContext) {
         init_test(cx);
@@ -1485,7 +1070,7 @@ mod tests {
 
         let fs = FakeFs::new(cx.executor());
         fs.insert_tree(
-            path!("/workspace"),
+            path!("/root"),
             json!({
                 "project_a": { "main.rs": "fn main() {}" },
                 "project_b": { "lib.rs": "pub fn lib() {}" }
@@ -1496,8 +1081,8 @@ mod tests {
         let project = Project::test(
             fs,
             [
-                path!("/workspace/project_a").as_ref(),
-                path!("/workspace/project_b").as_ref(),
+                path!("/root/project_a").as_ref(),
+                path!("/root/project_b").as_ref(),
             ],
             cx,
         )
@@ -1521,7 +1106,7 @@ mod tests {
 
         trusted_worktrees.update(cx, |store, cx| {
             store.trust(
-                HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/workspace")))]),
+                HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/root")))]),
                 None,
                 cx,
             );
@@ -1596,12 +1181,6 @@ mod tests {
                 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
             assert!(!can_trust, "worktree should be restricted initially");
         }
-        let can_trust_workspace =
-            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
-        assert!(
-            !can_trust_workspace,
-            "workspace should be restricted initially"
-        );
 
         let has_restricted = trusted_worktrees.read_with(cx, |store, cx| {
             store.has_restricted_worktrees(&worktree_store, cx)
@@ -1623,13 +1202,6 @@ mod tests {
             );
         }
 
-        let can_trust_workspace =
-            trusted_worktrees.update(cx, |store, cx| store.can_trust_workspace(None, cx));
-        assert!(
-            can_trust_workspace,
-            "workspace should be trusted after auto_trust_all"
-        );
-
         let has_restricted_after = trusted_worktrees.read_with(cx, |store, cx| {
             store.has_restricted_worktrees(&worktree_store, cx)
         });
@@ -1649,100 +1221,6 @@ mod tests {
         );
     }
 
-    #[gpui::test]
-    async fn test_wait_for_global_trust_already_trusted(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
-            .await;
-
-        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
-        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
-
-        let trusted_worktrees = init_trust_global(worktree_store, cx);
-
-        trusted_worktrees.update(cx, |store, cx| {
-            store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
-        });
-
-        let task = cx.update(|cx| wait_for_workspace_trust(None::<RemoteHostLocation>, "test", cx));
-        assert!(task.is_none(), "should return None when already trusted");
-    }
-
-    #[gpui::test]
-    async fn test_wait_for_workspace_trust_resolves_on_trust(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
-            .await;
-
-        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
-        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
-
-        let trusted_worktrees = init_trust_global(worktree_store, cx);
-
-        let task = cx.update(|cx| wait_for_workspace_trust(None::<RemoteHostLocation>, "test", cx));
-        assert!(
-            task.is_some(),
-            "should return Some(Task) when not yet trusted"
-        );
-
-        let task = task.unwrap();
-
-        cx.executor().run_until_parked();
-
-        trusted_worktrees.update(cx, |store, cx| {
-            store.trust(HashSet::from_iter([PathTrust::Workspace]), None, cx);
-        });
-
-        cx.executor().run_until_parked();
-        task.await;
-    }
-
-    #[gpui::test]
-    async fn test_wait_for_default_workspace_trust_resolves_on_directory_worktree_trust(
-        cx: &mut TestAppContext,
-    ) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/root"), json!({ "main.rs": "fn main() {}" }))
-            .await;
-
-        let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
-        let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
-        let worktree_id = worktree_store.read_with(cx, |store, cx| {
-            let worktree = store.worktrees().next().unwrap();
-            assert!(!worktree.read(cx).is_single_file());
-            worktree.read(cx).id()
-        });
-
-        let trusted_worktrees = init_trust_global(worktree_store, cx);
-
-        let task = cx.update(|cx| wait_for_default_workspace_trust("test", cx));
-        assert!(
-            task.is_some(),
-            "should return Some(Task) when not yet trusted"
-        );
-
-        let task = task.unwrap();
-
-        cx.executor().run_until_parked();
-
-        trusted_worktrees.update(cx, |store, cx| {
-            store.trust(
-                HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
-                None,
-                cx,
-            );
-        });
-
-        cx.executor().run_until_parked();
-        task.await;
-    }
-
     #[gpui::test]
     async fn test_trust_restrict_trust_cycle(cx: &mut TestAppContext) {
         init_test(cx);
@@ -1877,36 +1355,11 @@ mod tests {
         let trusted_worktrees = init_trust_global(worktree_store, cx);
 
         let host_a: Option<RemoteHostLocation> = None;
-        let host_b = Some(RemoteHostLocation {
-            user_name: Some("user".into()),
-            host_identifier: "remote-host".into(),
-        });
 
         let can_trust_local =
             trusted_worktrees.update(cx, |store, cx| store.can_trust(local_worktree, cx));
         assert!(!can_trust_local, "local worktree restricted on host_a");
 
-        trusted_worktrees.update(cx, |store, cx| {
-            store.trust(
-                HashSet::from_iter([PathTrust::Workspace]),
-                host_b.clone(),
-                cx,
-            );
-        });
-
-        let can_trust_workspace_a = trusted_worktrees.update(cx, |store, cx| {
-            store.can_trust_workspace(host_a.clone(), cx)
-        });
-        assert!(
-            !can_trust_workspace_a,
-            "host_a workspace should still be restricted"
-        );
-
-        let can_trust_workspace_b = trusted_worktrees.update(cx, |store, cx| {
-            store.can_trust_workspace(host_b.clone(), cx)
-        });
-        assert!(can_trust_workspace_b, "host_b workspace should be trusted");
-
         trusted_worktrees.update(cx, |store, cx| {
             store.trust(
                 HashSet::from_iter([PathTrust::Worktree(local_worktree)]),

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

@@ -62,7 +62,7 @@ fn main() -> Result<(), anyhow::Error> {
         let client = Client::production(cx);
         let http_client = FakeHttpClient::with_200_response();
         let (_, rx) = watch::channel(None);
-        let node = NodeRuntime::new(http_client, None, rx, None);
+        let node = NodeRuntime::new(http_client, None, rx);
         let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
         let registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
         let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));

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

@@ -880,7 +880,7 @@ impl ProjectPanel {
                                 });
                                 if !focus_opened_item {
                                     let focus_handle = project_panel.read(cx).focus_handle.clone();
-                                    window.focus(&focus_handle);
+                                    window.focus(&focus_handle, cx);
                                 }
                             }
                         }
@@ -1169,7 +1169,7 @@ impl ProjectPanel {
                 })
             });
 
-            window.focus(&context_menu.focus_handle(cx));
+            window.focus(&context_menu.focus_handle(cx), cx);
             let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
                 this.context_menu.take();
                 cx.notify();
@@ -1376,7 +1376,7 @@ impl ProjectPanel {
                 }
             });
             self.update_visible_entries(Some((worktree_id, entry_id)), false, false, window, cx);
-            window.focus(&self.focus_handle);
+            window.focus(&self.focus_handle, cx);
             cx.notify();
         }
     }
@@ -1399,7 +1399,7 @@ impl ProjectPanel {
                 }
             }
             self.update_visible_entries(Some((worktree_id, entry_id)), false, false, window, cx);
-            window.focus(&self.focus_handle);
+            window.focus(&self.focus_handle, cx);
             cx.notify();
         }
     }
@@ -1719,7 +1719,7 @@ impl ProjectPanel {
             };
             if let Some(existing) = worktree.read(cx).entry_for_path(&new_path) {
                 if existing.id == entry.id && refocus {
-                    window.focus(&self.focus_handle);
+                    window.focus(&self.focus_handle, cx);
                 }
                 return None;
             }
@@ -1730,7 +1730,7 @@ impl ProjectPanel {
         };
 
         if refocus {
-            window.focus(&self.focus_handle);
+            window.focus(&self.focus_handle, cx);
         }
         edit_state.processing_filename = Some(filename);
         cx.notify();
@@ -1839,7 +1839,7 @@ impl ProjectPanel {
             self.autoscroll(cx);
         }
 
-        window.focus(&self.focus_handle);
+        window.focus(&self.focus_handle, cx);
         cx.notify();
     }
 
@@ -3616,7 +3616,7 @@ impl ProjectPanel {
                 if this.update_visible_entries_task.focus_filename_editor {
                     this.update_visible_entries_task.focus_filename_editor = false;
                     this.filename_editor.update(cx, |editor, cx| {
-                        window.focus(&editor.focus_handle(cx));
+                        window.focus(&editor.focus_handle(cx), cx);
                     });
                 }
                 if this.update_visible_entries_task.autoscroll {
@@ -5952,7 +5952,7 @@ impl Render for ProjectPanel {
                                     cx.stop_propagation();
                                     this.state.selection = None;
                                     this.marked_entries.clear();
-                                    this.focus_handle(cx).focus(window);
+                                    this.focus_handle(cx).focus(window, cx);
                                 }))
                                 .on_mouse_down(
                                     MouseButton::Right,

crates/project_panel/src/project_panel_settings.rs πŸ”—

@@ -92,7 +92,13 @@ impl Settings for ProjectPanelSettings {
             entry_spacing: project_panel.entry_spacing.unwrap(),
             file_icons: project_panel.file_icons.unwrap(),
             folder_icons: project_panel.folder_icons.unwrap(),
-            git_status: project_panel.git_status.unwrap(),
+            git_status: project_panel.git_status.unwrap()
+                && content
+                    .git
+                    .unwrap()
+                    .enabled
+                    .unwrap()
+                    .is_git_status_enabled(),
             indent_size: project_panel.indent_size.unwrap(),
             indent_guides: IndentGuidesSettings {
                 show: project_panel.indent_guides.unwrap().show.unwrap(),

crates/prompt_store/Cargo.toml πŸ”—

@@ -28,6 +28,11 @@ parking_lot.workspace = true
 paths.workspace = true
 rope.workspace = true
 serde.workspace = true
+strum.workspace = true
 text.workspace = true
 util.workspace = true
 uuid.workspace = true
+
+[dev-dependencies]
+gpui = { workspace = true, features = ["test-support"] }
+tempfile.workspace = true

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

@@ -1,6 +1,6 @@
 mod prompts;
 
-use anyhow::{Context as _, Result, anyhow};
+use anyhow::{Result, anyhow};
 use chrono::{DateTime, Utc};
 use collections::HashMap;
 use futures::FutureExt as _;
@@ -23,6 +23,7 @@ use std::{
     path::PathBuf,
     sync::{Arc, atomic::AtomicBool},
 };
+use strum::{EnumIter, IntoEnumIterator as _};
 use text::LineEnding;
 use util::ResultExt;
 use uuid::Uuid;
@@ -51,12 +52,51 @@ pub struct PromptMetadata {
     pub saved_at: DateTime<Utc>,
 }
 
+impl PromptMetadata {
+    fn builtin(builtin: BuiltInPrompt) -> Self {
+        Self {
+            id: PromptId::BuiltIn(builtin),
+            title: Some(builtin.title().into()),
+            default: false,
+            saved_at: DateTime::default(),
+        }
+    }
+}
+
+/// Built-in prompts that have default content and can be customized by users.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter)]
+pub enum BuiltInPrompt {
+    CommitMessage,
+}
+
+impl BuiltInPrompt {
+    pub fn title(&self) -> &'static str {
+        match self {
+            Self::CommitMessage => "Commit message",
+        }
+    }
+
+    /// Returns the default content for this built-in prompt.
+    pub fn default_content(&self) -> &'static str {
+        match self {
+            Self::CommitMessage => include_str!("../../git_ui/src/commit_message_prompt.txt"),
+        }
+    }
+}
+
+impl std::fmt::Display for BuiltInPrompt {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::CommitMessage => write!(f, "Commit message"),
+        }
+    }
+}
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
 #[serde(tag = "kind")]
 pub enum PromptId {
     User { uuid: UserPromptId },
-    EditWorkflow,
-    CommitMessage,
+    BuiltIn(BuiltInPrompt),
 }
 
 impl PromptId {
@@ -64,32 +104,37 @@ impl PromptId {
         UserPromptId::new().into()
     }
 
-    pub fn user_id(&self) -> Option<UserPromptId> {
+    pub fn as_user(&self) -> Option<UserPromptId> {
         match self {
             Self::User { uuid } => Some(*uuid),
-            _ => None,
+            Self::BuiltIn { .. } => None,
         }
     }
 
-    pub fn is_built_in(&self) -> bool {
+    pub fn as_built_in(&self) -> Option<BuiltInPrompt> {
         match self {
-            Self::User { .. } => false,
-            Self::EditWorkflow | Self::CommitMessage => true,
+            Self::User { .. } => None,
+            Self::BuiltIn(builtin) => Some(*builtin),
         }
     }
 
+    pub fn is_built_in(&self) -> bool {
+        matches!(self, Self::BuiltIn { .. })
+    }
+
     pub fn can_edit(&self) -> bool {
         match self {
-            Self::User { .. } | Self::CommitMessage => true,
-            Self::EditWorkflow => false,
+            Self::User { .. } => true,
+            Self::BuiltIn(builtin) => match builtin {
+                BuiltInPrompt::CommitMessage => true,
+            },
         }
     }
+}
 
-    pub fn default_content(&self) -> Option<&'static str> {
-        match self {
-            Self::User { .. } | Self::EditWorkflow => None,
-            Self::CommitMessage => Some(include_str!("../../git_ui/src/commit_message_prompt.txt")),
-        }
+impl From<BuiltInPrompt> for PromptId {
+    fn from(builtin: BuiltInPrompt) -> Self {
+        PromptId::BuiltIn(builtin)
     }
 }
 
@@ -119,8 +164,7 @@ impl std::fmt::Display for PromptId {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
             PromptId::User { uuid } => write!(f, "{}", uuid.0),
-            PromptId::EditWorkflow => write!(f, "Edit workflow"),
-            PromptId::CommitMessage => write!(f, "Commit message"),
+            PromptId::BuiltIn(builtin) => write!(f, "{}", builtin),
         }
     }
 }
@@ -153,6 +197,16 @@ impl MetadataCache {
             cache.metadata.push(metadata.clone());
             cache.metadata_by_id.insert(prompt_id, metadata);
         }
+
+        // Insert all the built-in prompts that were not customized by the user
+        for builtin in BuiltInPrompt::iter() {
+            let builtin_id = PromptId::BuiltIn(builtin);
+            if !cache.metadata_by_id.contains_key(&builtin_id) {
+                let metadata = PromptMetadata::builtin(builtin);
+                cache.metadata.push(metadata.clone());
+                cache.metadata_by_id.insert(builtin_id, metadata);
+            }
+        }
         cache.sort();
         Ok(cache)
     }
@@ -201,31 +255,6 @@ impl PromptStore {
             let mut txn = db_env.write_txn()?;
             let metadata = db_env.create_database(&mut txn, Some("metadata.v2"))?;
             let bodies = db_env.create_database(&mut txn, Some("bodies.v2"))?;
-
-            // Remove edit workflow prompt, as we decided to opt into it using
-            // a slash command instead.
-            metadata.delete(&mut txn, &PromptId::EditWorkflow).ok();
-            bodies.delete(&mut txn, &PromptId::EditWorkflow).ok();
-
-            // Insert default commit message prompt if not present
-            if metadata.get(&txn, &PromptId::CommitMessage)?.is_none() {
-                metadata.put(
-                    &mut txn,
-                    &PromptId::CommitMessage,
-                    &PromptMetadata {
-                        id: PromptId::CommitMessage,
-                        title: Some("Git Commit Message".into()),
-                        default: false,
-                        saved_at: Utc::now(),
-                    },
-                )?;
-            }
-            if bodies.get(&txn, &PromptId::CommitMessage)?.is_none() {
-                let commit_message_prompt =
-                    include_str!("../../git_ui/src/commit_message_prompt.txt");
-                bodies.put(&mut txn, &PromptId::CommitMessage, commit_message_prompt)?;
-            }
-
             txn.commit()?;
 
             Self::upgrade_dbs(&db_env, metadata, bodies).log_err();
@@ -318,7 +347,16 @@ impl PromptStore {
         let bodies = self.bodies;
         cx.background_spawn(async move {
             let txn = env.read_txn()?;
-            let mut prompt = bodies.get(&txn, &id)?.context("prompt not found")?.into();
+            let mut prompt: String = match bodies.get(&txn, &id)? {
+                Some(body) => body.into(),
+                None => {
+                    if let Some(built_in) = id.as_built_in() {
+                        built_in.default_content().into()
+                    } else {
+                        anyhow::bail!("prompt not found")
+                    }
+                }
+            };
             LineEnding::normalize(&mut prompt);
             Ok(prompt)
         })
@@ -363,11 +401,6 @@ impl PromptStore {
         })
     }
 
-    /// Returns the number of prompts in the store.
-    pub fn prompt_count(&self) -> usize {
-        self.metadata_cache.read().metadata.len()
-    }
-
     pub fn metadata(&self, id: PromptId) -> Option<PromptMetadata> {
         self.metadata_cache.read().metadata_by_id.get(&id).cloned()
     }
@@ -436,23 +469,38 @@ impl PromptStore {
             return Task::ready(Err(anyhow!("this prompt cannot be edited")));
         }
 
-        let prompt_metadata = PromptMetadata {
-            id,
-            title,
-            default,
-            saved_at: Utc::now(),
+        let body = body.to_string();
+        let is_default_content = id
+            .as_built_in()
+            .is_some_and(|builtin| body.trim() == builtin.default_content().trim());
+
+        let metadata = if let Some(builtin) = id.as_built_in() {
+            PromptMetadata::builtin(builtin)
+        } else {
+            PromptMetadata {
+                id,
+                title,
+                default,
+                saved_at: Utc::now(),
+            }
         };
-        self.metadata_cache.write().insert(prompt_metadata.clone());
+
+        self.metadata_cache.write().insert(metadata.clone());
 
         let db_connection = self.env.clone();
         let bodies = self.bodies;
-        let metadata = self.metadata;
+        let metadata_db = self.metadata;
 
         let task = cx.background_spawn(async move {
             let mut txn = db_connection.write_txn()?;
 
-            metadata.put(&mut txn, &id, &prompt_metadata)?;
-            bodies.put(&mut txn, &id, &body.to_string())?;
+            if is_default_content {
+                metadata_db.delete(&mut txn, &id)?;
+                bodies.delete(&mut txn, &id)?;
+            } else {
+                metadata_db.put(&mut txn, &id, &metadata)?;
+                bodies.put(&mut txn, &id, &body)?;
+            }
 
             txn.commit()?;
 
@@ -514,3 +562,122 @@ impl PromptStore {
 pub struct GlobalPromptStore(Shared<Task<Result<Entity<PromptStore>, Arc<anyhow::Error>>>>);
 
 impl Global for GlobalPromptStore {}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::TestAppContext;
+
+    #[gpui::test]
+    async fn test_built_in_prompt_load_save(cx: &mut TestAppContext) {
+        cx.executor().allow_parking();
+
+        let temp_dir = tempfile::tempdir().unwrap();
+        let db_path = temp_dir.path().join("prompts-db");
+
+        let store = cx.update(|cx| PromptStore::new(db_path, cx)).await.unwrap();
+        let store = cx.new(|_cx| store);
+
+        let commit_message_id = PromptId::BuiltIn(BuiltInPrompt::CommitMessage);
+
+        let loaded_content = store
+            .update(cx, |store, cx| store.load(commit_message_id, cx))
+            .await
+            .unwrap();
+
+        let mut expected_content = BuiltInPrompt::CommitMessage.default_content().to_string();
+        LineEnding::normalize(&mut expected_content);
+        assert_eq!(
+            loaded_content.trim(),
+            expected_content.trim(),
+            "Loading a built-in prompt not in DB should return default content"
+        );
+
+        let metadata = store.read_with(cx, |store, _| store.metadata(commit_message_id));
+        assert!(
+            metadata.is_some(),
+            "Built-in prompt should always have metadata"
+        );
+        assert!(
+            store.read_with(cx, |store, _| {
+                store
+                    .metadata_cache
+                    .read()
+                    .metadata_by_id
+                    .contains_key(&commit_message_id)
+            }),
+            "Built-in prompt should always be in cache"
+        );
+
+        let custom_content = "Custom commit message prompt";
+        store
+            .update(cx, |store, cx| {
+                store.save(
+                    commit_message_id,
+                    Some("Commit message".into()),
+                    false,
+                    Rope::from(custom_content),
+                    cx,
+                )
+            })
+            .await
+            .unwrap();
+
+        let loaded_custom = store
+            .update(cx, |store, cx| store.load(commit_message_id, cx))
+            .await
+            .unwrap();
+        assert_eq!(
+            loaded_custom.trim(),
+            custom_content.trim(),
+            "Custom content should be loaded after saving"
+        );
+
+        assert!(
+            store
+                .read_with(cx, |store, _| store.metadata(commit_message_id))
+                .is_some(),
+            "Built-in prompt should have metadata after customization"
+        );
+
+        store
+            .update(cx, |store, cx| {
+                store.save(
+                    commit_message_id,
+                    Some("Commit message".into()),
+                    false,
+                    Rope::from(BuiltInPrompt::CommitMessage.default_content()),
+                    cx,
+                )
+            })
+            .await
+            .unwrap();
+
+        let metadata_after_reset =
+            store.read_with(cx, |store, _| store.metadata(commit_message_id));
+        assert!(
+            metadata_after_reset.is_some(),
+            "Built-in prompt should still have metadata after reset"
+        );
+        assert_eq!(
+            metadata_after_reset
+                .as_ref()
+                .and_then(|m| m.title.as_ref().map(|t| t.as_ref())),
+            Some("Commit message"),
+            "Built-in prompt should have default title after reset"
+        );
+
+        let loaded_after_reset = store
+            .update(cx, |store, cx| store.load(commit_message_id, cx))
+            .await
+            .unwrap();
+        let mut expected_content_after_reset =
+            BuiltInPrompt::CommitMessage.default_content().to_string();
+        LineEnding::normalize(&mut expected_content_after_reset);
+        assert_eq!(
+            loaded_after_reset.trim(),
+            expected_content_after_reset.trim(),
+            "After saving default content, load should return default"
+        );
+    }
+}

crates/prompt_store/src/prompts.rs πŸ”—

@@ -112,7 +112,7 @@ pub struct ContentPromptContextV2 {
     pub language_name: Option<String>,
     pub is_truncated: bool,
     pub document_content: String,
-    pub rewrite_section: Option<String>,
+    pub rewrite_section: String,
     pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
 }
 
@@ -310,7 +310,6 @@ impl PromptBuilder {
         };
 
         const MAX_CTX: usize = 50000;
-        let is_insert = range.is_empty();
         let mut is_truncated = false;
 
         let before_range = 0..range.start;
@@ -335,28 +334,19 @@ impl PromptBuilder {
         for chunk in buffer.text_for_range(truncated_before) {
             document_content.push_str(chunk);
         }
-        if is_insert {
-            document_content.push_str("<insert_here></insert_here>");
-        } else {
-            document_content.push_str("<rewrite_this>\n");
-            for chunk in buffer.text_for_range(range.clone()) {
-                document_content.push_str(chunk);
-            }
-            document_content.push_str("\n</rewrite_this>");
+
+        document_content.push_str("<rewrite_this>\n");
+        for chunk in buffer.text_for_range(range.clone()) {
+            document_content.push_str(chunk);
         }
+        document_content.push_str("\n</rewrite_this>");
+
         for chunk in buffer.text_for_range(truncated_after) {
             document_content.push_str(chunk);
         }
 
-        let rewrite_section = if !is_insert {
-            let mut section = String::new();
-            for chunk in buffer.text_for_range(range.clone()) {
-                section.push_str(chunk);
-            }
-            Some(section)
-        } else {
-            None
-        };
+        let rewrite_section: String = buffer.text_for_range(range.clone()).collect();
+
         let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
         let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
             .map(|entry| {

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

@@ -166,14 +166,16 @@ message TrustWorktrees {
 
 message PathTrust {
     oneof content {
-        uint64 workspace = 1;
         uint64 worktree_id = 2;
         string abs_path = 3;
     }
+
+    reserved 1;
 }
 
 message RestrictWorktrees {
     uint64 project_id = 1;
-    bool restrict_workspace = 2;
     repeated uint64 worktree_ids = 3;
+
+    reserved 2;
 }

crates/recent_projects/src/remote_connections.rs πŸ”—

@@ -52,7 +52,7 @@ impl SshSettings {
 
     pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) {
         for conn in self.ssh_connections() {
-            if conn.host == options.host
+            if conn.host == options.host.to_string()
                 && conn.username == options.username
                 && conn.port == options.port
             {
@@ -72,7 +72,7 @@ impl SshSettings {
         username: Option<String>,
     ) -> SshConnectionOptions {
         let mut options = SshConnectionOptions {
-            host,
+            host: host.into(),
             port,
             username,
             ..Default::default()
@@ -209,7 +209,7 @@ impl RemoteConnectionPrompt {
         let markdown = cx.new(|cx| Markdown::new_text(prompt.into(), cx));
         self.prompt = Some((markdown, tx));
         self.status_message.take();
-        window.focus(&self.editor.focus_handle(cx));
+        window.focus(&self.editor.focus_handle(cx), cx);
         cx.notify();
     }
 
@@ -533,8 +533,8 @@ impl remote::RemoteClientDelegate for RemoteClientDelegate {
             AutoUpdater::download_remote_server_release(
                 release_channel,
                 version.clone(),
-                platform.os,
-                platform.arch,
+                platform.os.as_str(),
+                platform.arch.as_str(),
                 move |status, cx| this.set_status(Some(status), cx),
                 cx,
             )
@@ -564,8 +564,8 @@ impl remote::RemoteClientDelegate for RemoteClientDelegate {
             AutoUpdater::get_remote_server_release_url(
                 release_channel,
                 version,
-                platform.os,
-                platform.arch,
+                platform.os.as_str(),
+                platform.arch.as_str(),
                 cx,
             )
             .await

crates/recent_projects/src/remote_servers.rs πŸ”—

@@ -76,7 +76,7 @@ impl CreateRemoteServer {
     fn new(window: &mut Window, cx: &mut App) -> Self {
         let address_editor = cx.new(|cx| Editor::single_line(window, cx));
         address_editor.update(cx, |this, cx| {
-            this.focus_handle(cx).focus(window);
+            this.focus_handle(cx).focus(window, cx);
         });
         Self {
             address_editor,
@@ -107,7 +107,7 @@ struct CreateRemoteDevContainer {
 impl CreateRemoteDevContainer {
     fn new(window: &mut Window, cx: &mut Context<RemoteServerProjects>) -> Self {
         let entries = std::array::from_fn(|_| NavigableEntry::focusable(cx));
-        entries[0].focus_handle.focus(window);
+        entries[0].focus_handle.focus(window, cx);
         Self {
             entries,
             progress: DevContainerCreationProgress::Initial,
@@ -199,7 +199,7 @@ impl EditNicknameState {
                 this.set_text(starting_text, window, cx);
             }
         });
-        this.editor.focus_handle(cx).focus(window);
+        this.editor.focus_handle(cx).focus(window, cx);
         this
     }
 }
@@ -792,7 +792,7 @@ impl RemoteServerProjects {
                         this.retained_connections.push(client);
                         this.add_ssh_server(connection_options, cx);
                         this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
-                        this.focus_handle(cx).focus(window);
+                        this.focus_handle(cx).focus(window, cx);
                         cx.notify()
                     })
                     .log_err(),
@@ -875,7 +875,7 @@ impl RemoteServerProjects {
 
                     crate::add_wsl_distro(fs, &connection_options, cx);
                     this.mode = Mode::default_mode(&BTreeSet::new(), cx);
-                    this.focus_handle(cx).focus(window);
+                    this.focus_handle(cx).focus(window, cx);
                     cx.notify();
                 }),
                 _ => this.update(cx, |this, cx| {
@@ -924,7 +924,7 @@ impl RemoteServerProjects {
                 return;
             }
         });
-        self.focus_handle(cx).focus(window);
+        self.focus_handle(cx).focus(window, cx);
         cx.notify();
     }
 
@@ -933,7 +933,7 @@ impl RemoteServerProjects {
             CreateRemoteDevContainer::new(window, cx)
                 .progress(DevContainerCreationProgress::Creating),
         );
-        self.focus_handle(cx).focus(window);
+        self.focus_handle(cx).focus(window, cx);
         cx.notify();
     }
 
@@ -1068,7 +1068,7 @@ impl RemoteServerProjects {
                     }
                 });
                 self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
-                self.focus_handle.focus(window);
+                self.focus_handle.focus(window, cx);
             }
             #[cfg(target_os = "windows")]
             Mode::AddWslDistro(state) => {
@@ -1094,7 +1094,7 @@ impl RemoteServerProjects {
             }
             _ => {
                 self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
-                self.focus_handle(cx).focus(window);
+                self.focus_handle(cx).focus(window, cx);
                 cx.notify();
             }
         }
@@ -1518,7 +1518,7 @@ impl RemoteServerProjects {
                 .ssh_connections
                 .get_or_insert(Default::default())
                 .push(SshConnection {
-                    host: SharedString::from(connection_options.host),
+                    host: SharedString::from(connection_options.host.to_string()),
                     username: connection_options.username,
                     port: connection_options.port,
                     projects: BTreeSet::new(),
@@ -1640,7 +1640,7 @@ impl RemoteServerProjects {
     ) -> impl IntoElement {
         match &state.progress {
             DevContainerCreationProgress::Error(message) => {
-                self.focus_handle(cx).focus(window);
+                self.focus_handle(cx).focus(window, cx);
                 return div()
                     .track_focus(&self.focus_handle(cx))
                     .size_full()
@@ -1952,7 +1952,7 @@ impl RemoteServerProjects {
         let connection_prompt = state.connection_prompt.clone();
 
         state.picker.update(cx, |picker, cx| {
-            picker.focus_handle(cx).focus(window);
+            picker.focus_handle(cx).focus(window, cx);
         });
 
         v_flex()
@@ -1983,7 +1983,7 @@ impl RemoteServerProjects {
                 .size_full()
                 .child(match &options {
                     ViewServerOptionsState::Ssh { connection, .. } => SshConnectionHeader {
-                        connection_string: connection.host.clone().into(),
+                        connection_string: connection.host.to_string().into(),
                         paths: Default::default(),
                         nickname: connection.nickname.clone().map(|s| s.into()),
                         is_wsl: false,
@@ -2148,7 +2148,7 @@ impl RemoteServerProjects {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
-        let connection_string = SharedString::new(connection.host.clone());
+        let connection_string = SharedString::new(connection.host.to_string());
 
         v_flex()
             .child({
@@ -2659,7 +2659,7 @@ impl RemoteServerProjects {
 
         self.add_ssh_server(
             SshConnectionOptions {
-                host: ssh_config_host.to_string(),
+                host: ssh_config_host.to_string().into(),
                 ..SshConnectionOptions::default()
             },
             cx,
@@ -2752,7 +2752,7 @@ impl Render for RemoteServerProjects {
             .on_action(cx.listener(Self::cancel))
             .on_action(cx.listener(Self::confirm))
             .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
-                this.focus_handle(cx).focus(window);
+                this.focus_handle(cx).focus(window, cx);
             }))
             .on_mouse_down_out(cx.listener(|this, _, _, cx| {
                 if matches!(this.mode, Mode::Default(_)) {

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

@@ -7,8 +7,9 @@ mod transport;
 #[cfg(target_os = "windows")]
 pub use remote_client::OpenWslPath;
 pub use remote_client::{
-    ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent,
-    RemoteConnection, RemoteConnectionOptions, RemotePlatform, connect,
+    ConnectionIdentifier, ConnectionState, RemoteArch, RemoteClient, RemoteClientDelegate,
+    RemoteClientEvent, RemoteConnection, RemoteConnectionOptions, RemoteOs, RemotePlatform,
+    connect,
 };
 pub use transport::docker::DockerConnectionOptions;
 pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption};

crates/remote/src/remote_client.rs πŸ”—

@@ -49,10 +49,58 @@ use util::{
     paths::{PathStyle, RemotePathBuf},
 };
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum RemoteOs {
+    Linux,
+    MacOs,
+    Windows,
+}
+
+impl RemoteOs {
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            RemoteOs::Linux => "linux",
+            RemoteOs::MacOs => "macos",
+            RemoteOs::Windows => "windows",
+        }
+    }
+
+    pub fn is_windows(&self) -> bool {
+        matches!(self, RemoteOs::Windows)
+    }
+}
+
+impl std::fmt::Display for RemoteOs {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(self.as_str())
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum RemoteArch {
+    X86_64,
+    Aarch64,
+}
+
+impl RemoteArch {
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            RemoteArch::X86_64 => "x86_64",
+            RemoteArch::Aarch64 => "aarch64",
+        }
+    }
+}
+
+impl std::fmt::Display for RemoteArch {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(self.as_str())
+    }
+}
+
 #[derive(Copy, Clone, Debug)]
 pub struct RemotePlatform {
-    pub os: &'static str,
-    pub arch: &'static str,
+    pub os: RemoteOs,
+    pub arch: RemoteArch,
 }
 
 #[derive(Clone, Debug)]
@@ -89,7 +137,8 @@ pub trait RemoteClientDelegate: Send + Sync {
 const MAX_MISSED_HEARTBEATS: usize = 5;
 const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
 const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5);
-const INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60);
+const INITIAL_CONNECTION_TIMEOUT: Duration =
+    Duration::from_secs(if cfg!(debug_assertions) { 5 } else { 60 });
 
 const MAX_RECONNECT_ATTEMPTS: usize = 3;
 
@@ -921,10 +970,12 @@ impl RemoteClient {
         client_cx: &mut gpui::TestAppContext,
         server_cx: &mut gpui::TestAppContext,
     ) -> (RemoteConnectionOptions, AnyProtoClient) {
+        use crate::transport::ssh::SshConnectionHost;
+
         let port = client_cx
             .update(|cx| cx.default_global::<ConnectionPool>().connections.len() as u16 + 1);
         let opts = RemoteConnectionOptions::Ssh(SshConnectionOptions {
-            host: "<fake>".to_string(),
+            host: SshConnectionHost::from("<fake>".to_string()),
             port: Some(port),
             ..Default::default()
         });
@@ -1089,7 +1140,7 @@ pub enum RemoteConnectionOptions {
 impl RemoteConnectionOptions {
     pub fn display_name(&self) -> String {
         match self {
-            RemoteConnectionOptions::Ssh(opts) => opts.host.clone(),
+            RemoteConnectionOptions::Ssh(opts) => opts.host.to_string(),
             RemoteConnectionOptions::Wsl(opts) => opts.distro_name.clone(),
             RemoteConnectionOptions::Docker(opts) => opts.name.clone(),
         }

crates/remote/src/transport.rs πŸ”—

@@ -1,5 +1,5 @@
 use crate::{
-    RemotePlatform,
+    RemoteArch, RemoteOs, RemotePlatform,
     json_log::LogRecord,
     protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message},
 };
@@ -26,8 +26,8 @@ fn parse_platform(output: &str) -> Result<RemotePlatform> {
     };
 
     let os = match os {
-        "Darwin" => "macos",
-        "Linux" => "linux",
+        "Darwin" => RemoteOs::MacOs,
+        "Linux" => RemoteOs::Linux,
         _ => anyhow::bail!(
             "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development"
         ),
@@ -39,9 +39,9 @@ fn parse_platform(output: &str) -> Result<RemotePlatform> {
         || arch.starts_with("arm64")
         || arch.starts_with("aarch64")
     {
-        "aarch64"
+        RemoteArch::Aarch64
     } else if arch.starts_with("x86") {
-        "x86_64"
+        RemoteArch::X86_64
     } else {
         anyhow::bail!(
             "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development"
@@ -193,7 +193,8 @@ async fn build_remote_server_from_source(
             .await?;
         anyhow::ensure!(
             output.status.success(),
-            "Failed to run command: {command:?}"
+            "Failed to run command: {command:?}: output: {}",
+            String::from_utf8_lossy(&output.stderr)
         );
         Ok(())
     }
@@ -203,14 +204,15 @@ async fn build_remote_server_from_source(
         "{}-{}",
         platform.arch,
         match platform.os {
-            "linux" =>
+            RemoteOs::Linux =>
                 if use_musl {
                     "unknown-linux-musl"
                 } else {
                     "unknown-linux-gnu"
                 },
-            "macos" => "apple-darwin",
-            _ => anyhow::bail!("can't cross compile for: {:?}", platform),
+            RemoteOs::MacOs => "apple-darwin",
+            RemoteOs::Windows if cfg!(windows) => "pc-windows-msvc",
+            RemoteOs::Windows => "pc-windows-gnu",
         }
     );
     let mut rust_flags = match std::env::var("RUSTFLAGS") {
@@ -221,7 +223,7 @@ async fn build_remote_server_from_source(
             String::new()
         }
     };
-    if platform.os == "linux" && use_musl {
+    if platform.os == RemoteOs::Linux && use_musl {
         rust_flags.push_str(" -C target-feature=+crt-static");
 
         if let Ok(path) = std::env::var("ZED_ZSTD_MUSL_LIB") {
@@ -232,7 +234,9 @@ async fn build_remote_server_from_source(
         rust_flags.push_str(" -C link-arg=-fuse-ld=mold");
     }
 
-    if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
+    if platform.arch.as_str() == std::env::consts::ARCH
+        && platform.os.as_str() == std::env::consts::OS
+    {
         delegate.set_status(Some("Building remote server binary from source"), cx);
         log::info!("building remote server binary from source");
         run_cmd(
@@ -308,7 +312,8 @@ async fn build_remote_server_from_source(
         .join("remote_server")
         .join(&triple)
         .join("debug")
-        .join("remote_server");
+        .join("remote_server")
+        .with_extension(if platform.os.is_windows() { "exe" } else { "" });
 
     let path = if !build_remote_server.contains("nocompress") {
         delegate.set_status(Some("Compressing binary"), cx);
@@ -374,35 +379,44 @@ mod tests {
     #[test]
     fn test_parse_platform() {
         let result = parse_platform("Linux x86_64\n").unwrap();
-        assert_eq!(result.os, "linux");
-        assert_eq!(result.arch, "x86_64");
+        assert_eq!(result.os, RemoteOs::Linux);
+        assert_eq!(result.arch, RemoteArch::X86_64);
 
         let result = parse_platform("Darwin arm64\n").unwrap();
-        assert_eq!(result.os, "macos");
-        assert_eq!(result.arch, "aarch64");
+        assert_eq!(result.os, RemoteOs::MacOs);
+        assert_eq!(result.arch, RemoteArch::Aarch64);
 
         let result = parse_platform("Linux x86_64").unwrap();
-        assert_eq!(result.os, "linux");
-        assert_eq!(result.arch, "x86_64");
+        assert_eq!(result.os, RemoteOs::Linux);
+        assert_eq!(result.arch, RemoteArch::X86_64);
 
         let result = parse_platform("some shell init output\nLinux aarch64\n").unwrap();
-        assert_eq!(result.os, "linux");
-        assert_eq!(result.arch, "aarch64");
+        assert_eq!(result.os, RemoteOs::Linux);
+        assert_eq!(result.arch, RemoteArch::Aarch64);
 
         let result = parse_platform("some shell init output\nLinux aarch64").unwrap();
-        assert_eq!(result.os, "linux");
-        assert_eq!(result.arch, "aarch64");
+        assert_eq!(result.os, RemoteOs::Linux);
+        assert_eq!(result.arch, RemoteArch::Aarch64);
 
-        assert_eq!(parse_platform("Linux armv8l\n").unwrap().arch, "aarch64");
-        assert_eq!(parse_platform("Linux aarch64\n").unwrap().arch, "aarch64");
-        assert_eq!(parse_platform("Linux x86_64\n").unwrap().arch, "x86_64");
+        assert_eq!(
+            parse_platform("Linux armv8l\n").unwrap().arch,
+            RemoteArch::Aarch64
+        );
+        assert_eq!(
+            parse_platform("Linux aarch64\n").unwrap().arch,
+            RemoteArch::Aarch64
+        );
+        assert_eq!(
+            parse_platform("Linux x86_64\n").unwrap().arch,
+            RemoteArch::X86_64
+        );
 
         let result = parse_platform(
             r#"Linux x86_64 - What you're referring to as Linux, is in fact, GNU/Linux...\n"#,
         )
         .unwrap();
-        assert_eq!(result.os, "linux");
-        assert_eq!(result.arch, "x86_64");
+        assert_eq!(result.os, RemoteOs::Linux);
+        assert_eq!(result.arch, RemoteArch::X86_64);
 
         assert!(parse_platform("Windows x86_64\n").is_err());
         assert!(parse_platform("Linux armv7l\n").is_err());

crates/remote/src/transport/docker.rs πŸ”—

@@ -24,8 +24,8 @@ use gpui::{App, AppContext, AsyncApp, Task};
 use rpc::proto::Envelope;
 
 use crate::{
-    RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, RemotePlatform,
-    remote_client::CommandTemplate,
+    RemoteArch, RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, RemoteOs,
+    RemotePlatform, remote_client::CommandTemplate,
 };
 
 #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
@@ -70,7 +70,7 @@ impl DockerExecConnection {
         let remote_platform = this.check_remote_platform().await?;
 
         this.path_style = match remote_platform.os {
-            "windows" => Some(PathStyle::Windows),
+            RemoteOs::Windows => Some(PathStyle::Windows),
             _ => Some(PathStyle::Posix),
         };
 
@@ -124,8 +124,8 @@ impl DockerExecConnection {
         };
 
         let os = match os.trim() {
-            "Darwin" => "macos",
-            "Linux" => "linux",
+            "Darwin" => RemoteOs::MacOs,
+            "Linux" => RemoteOs::Linux,
             _ => anyhow::bail!(
                 "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development"
             ),
@@ -136,9 +136,9 @@ impl DockerExecConnection {
             || arch.starts_with("arm64")
             || arch.starts_with("aarch64")
         {
-            "aarch64"
+            RemoteArch::Aarch64
         } else if arch.starts_with("x86") {
-            "x86_64"
+            RemoteArch::X86_64
         } else {
             anyhow::bail!(
                 "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development"

crates/remote/src/transport/ssh.rs πŸ”—

@@ -1,5 +1,5 @@
 use crate::{
-    RemoteClientDelegate, RemotePlatform,
+    RemoteArch, RemoteClientDelegate, RemoteOs, RemotePlatform,
     remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions},
     transport::{parse_platform, parse_shell},
 };
@@ -23,6 +23,7 @@ use smol::{
     process::{self, Child, Stdio},
 };
 use std::{
+    net::IpAddr,
     path::{Path, PathBuf},
     sync::Arc,
     time::Instant,
@@ -47,9 +48,58 @@ pub(crate) struct SshRemoteConnection {
     _temp_dir: TempDir,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum SshConnectionHost {
+    IpAddr(IpAddr),
+    Hostname(String),
+}
+
+impl SshConnectionHost {
+    pub fn to_bracketed_string(&self) -> String {
+        match self {
+            Self::IpAddr(IpAddr::V4(ip)) => ip.to_string(),
+            Self::IpAddr(IpAddr::V6(ip)) => format!("[{}]", ip),
+            Self::Hostname(hostname) => hostname.clone(),
+        }
+    }
+
+    pub fn to_string(&self) -> String {
+        match self {
+            Self::IpAddr(ip) => ip.to_string(),
+            Self::Hostname(hostname) => hostname.clone(),
+        }
+    }
+}
+
+impl From<&str> for SshConnectionHost {
+    fn from(value: &str) -> Self {
+        if let Ok(address) = value.parse() {
+            Self::IpAddr(address)
+        } else {
+            Self::Hostname(value.to_string())
+        }
+    }
+}
+
+impl From<String> for SshConnectionHost {
+    fn from(value: String) -> Self {
+        if let Ok(address) = value.parse() {
+            Self::IpAddr(address)
+        } else {
+            Self::Hostname(value)
+        }
+    }
+}
+
+impl Default for SshConnectionHost {
+    fn default() -> Self {
+        Self::Hostname(Default::default())
+    }
+}
+
 #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
 pub struct SshConnectionOptions {
-    pub host: String,
+    pub host: SshConnectionHost,
     pub username: Option<String>,
     pub port: Option<u16>,
     pub password: Option<String>,
@@ -64,7 +114,7 @@ pub struct SshConnectionOptions {
 impl From<settings::SshConnection> for SshConnectionOptions {
     fn from(val: settings::SshConnection) -> Self {
         SshConnectionOptions {
-            host: val.host.into(),
+            host: val.host.to_string().into(),
             username: val.username,
             port: val.port,
             password: None,
@@ -96,7 +146,7 @@ impl MasterProcess {
         askpass_script_path: &std::ffi::OsStr,
         additional_args: Vec<String>,
         socket_path: &std::path::Path,
-        url: &str,
+        destination: &str,
     ) -> Result<Self> {
         let args = [
             "-N",
@@ -120,7 +170,7 @@ impl MasterProcess {
 
         master_process.arg(format!("ControlPath={}", socket_path.display()));
 
-        let process = master_process.arg(&url).spawn()?;
+        let process = master_process.arg(&destination).spawn()?;
 
         Ok(MasterProcess { process })
     }
@@ -143,7 +193,7 @@ impl MasterProcess {
     pub fn new(
         askpass_script_path: &std::ffi::OsStr,
         additional_args: Vec<String>,
-        url: &str,
+        destination: &str,
     ) -> Result<Self> {
         // On Windows, `ControlMaster` and `ControlPath` are not supported:
         // https://github.com/PowerShell/Win32-OpenSSH/issues/405
@@ -165,7 +215,7 @@ impl MasterProcess {
             .env("SSH_ASKPASS_REQUIRE", "force")
             .env("SSH_ASKPASS", askpass_script_path)
             .args(additional_args)
-            .arg(url)
+            .arg(destination)
             .args(args);
 
         let process = master_process.spawn()?;
@@ -352,30 +402,50 @@ impl RemoteConnection for SshRemoteConnection {
         delegate: Arc<dyn RemoteClientDelegate>,
         cx: &mut AsyncApp,
     ) -> Task<Result<i32>> {
+        const VARS: [&str; 3] = ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"];
         delegate.set_status(Some("Starting proxy"), cx);
 
         let Some(remote_binary_path) = self.remote_binary_path.clone() else {
             return Task::ready(Err(anyhow!("Remote binary path not set")));
         };
 
-        let mut proxy_args = vec![];
-        for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
-            if let Some(value) = std::env::var(env_var).ok() {
-                proxy_args.push(format!("{}='{}'", env_var, value));
+        let mut ssh_command = if self.ssh_platform.os.is_windows() {
+            // TODO: Set the `VARS` environment variables, we do not have `env` on windows
+            // so this needs a different approach
+            let mut proxy_args = vec![];
+            proxy_args.push("proxy".to_owned());
+            proxy_args.push("--identifier".to_owned());
+            proxy_args.push(unique_identifier);
+
+            if reconnect {
+                proxy_args.push("--reconnect".to_owned());
             }
-        }
-        proxy_args.push(remote_binary_path.display(self.path_style()).into_owned());
-        proxy_args.push("proxy".to_owned());
-        proxy_args.push("--identifier".to_owned());
-        proxy_args.push(unique_identifier);
+            self.socket.ssh_command(
+                self.ssh_shell_kind,
+                &remote_binary_path.display(self.path_style()),
+                &proxy_args,
+                false,
+            )
+        } else {
+            let mut proxy_args = vec![];
+            for env_var in VARS {
+                if let Some(value) = std::env::var(env_var).ok() {
+                    proxy_args.push(format!("{}='{}'", env_var, value));
+                }
+            }
+            proxy_args.push(remote_binary_path.display(self.path_style()).into_owned());
+            proxy_args.push("proxy".to_owned());
+            proxy_args.push("--identifier".to_owned());
+            proxy_args.push(unique_identifier);
 
-        if reconnect {
-            proxy_args.push("--reconnect".to_owned());
-        }
+            if reconnect {
+                proxy_args.push("--reconnect".to_owned());
+            }
+            self.socket
+                .ssh_command(self.ssh_shell_kind, "env", &proxy_args, false)
+        };
 
-        let ssh_proxy_process = match self
-            .socket
-            .ssh_command(self.ssh_shell_kind, "env", &proxy_args, false)
+        let ssh_proxy_process = match ssh_command
             // IMPORTANT: we kill this process when we drop the task that uses it.
             .kill_on_drop(true)
             .spawn()
@@ -412,7 +482,7 @@ impl SshRemoteConnection {
     ) -> Result<Self> {
         use askpass::AskPassResult;
 
-        let url = connection_options.ssh_url();
+        let destination = connection_options.ssh_destination();
 
         let temp_dir = tempfile::Builder::new()
             .prefix("zed-ssh-session")
@@ -437,14 +507,14 @@ impl SshRemoteConnection {
         let mut master_process = MasterProcess::new(
             askpass.script_path().as_ref(),
             connection_options.additional_args(),
-            &url,
+            &destination,
         )?;
         #[cfg(not(target_os = "windows"))]
         let mut master_process = MasterProcess::new(
             askpass.script_path().as_ref(),
             connection_options.additional_args(),
             &socket_path,
-            &url,
+            &destination,
         )?;
 
         let result = select_biased! {
@@ -495,22 +565,20 @@ impl SshRemoteConnection {
         .await?;
         drop(askpass);
 
-        let ssh_shell = socket.shell().await;
+        let is_windows = socket.probe_is_windows().await;
+        log::info!("Remote is windows: {}", is_windows);
+
+        let ssh_shell = socket.shell(is_windows).await;
         log::info!("Remote shell discovered: {}", ssh_shell);
-        let ssh_platform = socket.platform(ShellKind::new(&ssh_shell, false)).await?;
+
+        let ssh_shell_kind = ShellKind::new(&ssh_shell, is_windows);
+        let ssh_platform = socket.platform(ssh_shell_kind, is_windows).await?;
         log::info!("Remote platform discovered: {:?}", ssh_platform);
-        let ssh_path_style = match ssh_platform.os {
-            "windows" => PathStyle::Windows,
-            _ => PathStyle::Posix,
+
+        let (ssh_path_style, ssh_default_system_shell) = match ssh_platform.os {
+            RemoteOs::Windows => (PathStyle::Windows, ssh_shell.clone()),
+            _ => (PathStyle::Posix, String::from("/bin/sh")),
         };
-        let ssh_default_system_shell = String::from("/bin/sh");
-        let ssh_shell_kind = ShellKind::new(
-            &ssh_shell,
-            match ssh_platform.os {
-                "windows" => true,
-                _ => false,
-            },
-        );
 
         let mut this = Self {
             socket,
@@ -546,9 +614,14 @@ impl SshRemoteConnection {
             _ => version.to_string(),
         };
         let binary_name = format!(
-            "zed-remote-server-{}-{}",
+            "zed-remote-server-{}-{}{}",
             release_channel.dev_name(),
-            version_str
+            version_str,
+            if self.ssh_platform.os.is_windows() {
+                ".exe"
+            } else {
+                ""
+            }
         );
         let dst_path =
             paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap());
@@ -660,14 +733,19 @@ impl SshRemoteConnection {
         cx: &mut AsyncApp,
     ) -> Result<()> {
         if let Some(parent) = tmp_path_gz.parent() {
-            self.socket
+            let res = self
+                .socket
                 .run_command(
                     self.ssh_shell_kind,
                     "mkdir",
                     &["-p", parent.display(self.path_style()).as_ref()],
                     true,
                 )
-                .await?;
+                .await;
+            if !self.ssh_platform.os.is_windows() {
+                // mkdir fails on windows if the path already exists ...
+                res?;
+            }
         }
 
         delegate.set_status(Some("Downloading remote development server on host"), cx);
@@ -755,17 +833,24 @@ impl SshRemoteConnection {
         cx: &mut AsyncApp,
     ) -> Result<()> {
         if let Some(parent) = tmp_path_gz.parent() {
-            self.socket
+            let res = self
+                .socket
                 .run_command(
                     self.ssh_shell_kind,
                     "mkdir",
                     &["-p", parent.display(self.path_style()).as_ref()],
                     true,
                 )
-                .await?;
+                .await;
+            if !self.ssh_platform.os.is_windows() {
+                // mkdir fails on windows if the path already exists ...
+                res?;
+            }
         }
 
-        let src_stat = fs::metadata(&src_path).await?;
+        let src_stat = fs::metadata(&src_path)
+            .await
+            .with_context(|| format!("failed to get metadata for {:?}", src_path))?;
         let size = src_stat.len();
 
         let t0 = Instant::now();
@@ -816,7 +901,7 @@ impl SshRemoteConnection {
         };
         let args = shell_kind.args_for_shell(false, script.to_string());
         self.socket
-            .run_command(shell_kind, "sh", &args, true)
+            .run_command(self.ssh_shell_kind, "sh", &args, true)
             .await?;
         Ok(())
     }
@@ -840,7 +925,7 @@ impl SshRemoteConnection {
         }
         command.arg(src_path).arg(format!(
             "{}:{}",
-            self.socket.connection_options.scp_url(),
+            self.socket.connection_options.scp_destination(),
             dest_path_str
         ));
         command
@@ -856,7 +941,7 @@ impl SshRemoteConnection {
                 .unwrap_or_default(),
         );
         command.arg("-b").arg("-");
-        command.arg(self.socket.connection_options.scp_url());
+        command.arg(self.socket.connection_options.scp_destination());
         command.stdin(Stdio::piped());
         command
     }
@@ -986,7 +1071,7 @@ impl SshSocket {
         let separator = shell_kind.sequential_commands_separator();
         let to_run = format!("cd{separator} {to_run}");
         self.ssh_options(&mut command, true)
-            .arg(self.connection_options.ssh_url());
+            .arg(self.connection_options.ssh_destination());
         if !allow_pseudo_tty {
             command.arg("-T");
         }
@@ -1004,6 +1089,7 @@ impl SshSocket {
     ) -> Result<String> {
         let mut command = self.ssh_command(shell_kind, program, args, allow_pseudo_tty);
         let output = command.output().await?;
+        log::debug!("{:?}: {:?}", command, output);
         anyhow::ensure!(
             output.status.success(),
             "failed to run command {command:?}: {}",
@@ -1063,7 +1149,7 @@ impl SshSocket {
             "ControlMaster=no".to_string(),
             "-o".to_string(),
             format!("ControlPath={}", self.socket_path.display()),
-            self.connection_options.ssh_url(),
+            self.connection_options.ssh_destination(),
         ]);
         arguments
     }
@@ -1071,16 +1157,75 @@ impl SshSocket {
     #[cfg(target_os = "windows")]
     fn ssh_args(&self) -> Vec<String> {
         let mut arguments = self.connection_options.additional_args();
-        arguments.push(self.connection_options.ssh_url());
+        arguments.push(self.connection_options.ssh_destination());
         arguments
     }
 
-    async fn platform(&self, shell: ShellKind) -> Result<RemotePlatform> {
-        let output = self.run_command(shell, "uname", &["-sm"], false).await?;
+    async fn platform(&self, shell: ShellKind, is_windows: bool) -> Result<RemotePlatform> {
+        if is_windows {
+            self.platform_windows(shell).await
+        } else {
+            self.platform_posix(shell).await
+        }
+    }
+
+    async fn platform_posix(&self, shell: ShellKind) -> Result<RemotePlatform> {
+        let output = self
+            .run_command(shell, "uname", &["-sm"], false)
+            .await
+            .context("Failed to run 'uname -sm' to determine platform")?;
         parse_platform(&output)
     }
 
-    async fn shell(&self) -> String {
+    async fn platform_windows(&self, shell: ShellKind) -> Result<RemotePlatform> {
+        let output = self
+            .run_command(
+                shell,
+                "cmd",
+                &["/c", "echo", "%PROCESSOR_ARCHITECTURE%"],
+                false,
+            )
+            .await
+            .context(
+                "Failed to run 'echo %PROCESSOR_ARCHITECTURE%' to determine Windows architecture",
+            )?;
+
+        Ok(RemotePlatform {
+            os: RemoteOs::Windows,
+            arch: match output.trim() {
+                "AMD64" => RemoteArch::X86_64,
+                "ARM64" => RemoteArch::Aarch64,
+                arch => anyhow::bail!(
+                    "Prebuilt remote servers are not yet available for windows-{arch}. See https://zed.dev/docs/remote-development"
+                ),
+            },
+        })
+    }
+
+    /// Probes whether the remote host is running Windows.
+    ///
+    /// This is done by attempting to run a simple Windows-specific command.
+    /// If it succeeds and returns Windows-like output, we assume it's Windows.
+    async fn probe_is_windows(&self) -> bool {
+        match self
+            .run_command(ShellKind::PowerShell, "cmd", &["/c", "ver"], false)
+            .await
+        {
+            // Windows 'ver' command outputs something like "Microsoft Windows [Version 10.0.19045.5011]"
+            Ok(output) => output.trim().contains("indows"),
+            Err(_) => false,
+        }
+    }
+
+    async fn shell(&self, is_windows: bool) -> String {
+        if is_windows {
+            self.shell_windows().await
+        } else {
+            self.shell_posix().await
+        }
+    }
+
+    async fn shell_posix(&self) -> String {
         const DEFAULT_SHELL: &str = "sh";
         match self
             .run_command(ShellKind::Posix, "sh", &["-c", "echo $SHELL"], false)
@@ -1093,6 +1238,13 @@ impl SshSocket {
             }
         }
     }
+
+    async fn shell_windows(&self) -> String {
+        // powershell is always the default, and cannot really be removed from the system
+        // so we can rely on that fact and reasonably assume that we will be running in a
+        // powershell environment
+        "powershell.exe".to_owned()
+    }
 }
 
 fn parse_port_number(port_str: &str) -> Result<u16> {
@@ -1208,10 +1360,24 @@ impl SshConnectionOptions {
                 input = rest;
                 username = Some(u.to_string());
             }
-            if let Some((rest, p)) = input.split_once(':') {
+
+            // Handle port parsing, accounting for IPv6 addresses
+            // IPv6 addresses can be: 2001:db8::1 or [2001:db8::1]:22
+            if input.starts_with('[') {
+                if let Some((rest, p)) = input.rsplit_once("]:") {
+                    input = rest.strip_prefix('[').unwrap_or(rest);
+                    port = p.parse().ok();
+                } else if input.ends_with(']') {
+                    input = input.strip_prefix('[').unwrap_or(input);
+                    input = input.strip_suffix(']').unwrap_or(input);
+                }
+            } else if let Some((rest, p)) = input.rsplit_once(':')
+                && !rest.contains(":")
+            {
                 input = rest;
-                port = p.parse().ok()
+                port = p.parse().ok();
             }
+
             hostname = Some(input.to_string())
         }
 
@@ -1225,7 +1391,7 @@ impl SshConnectionOptions {
         };
 
         Ok(Self {
-            host: hostname,
+            host: hostname.into(),
             username,
             port,
             port_forwards,
@@ -1237,19 +1403,16 @@ impl SshConnectionOptions {
         })
     }
 
-    pub fn ssh_url(&self) -> String {
-        let mut result = String::from("ssh://");
+    pub fn ssh_destination(&self) -> String {
+        let mut result = String::default();
         if let Some(username) = &self.username {
             // Username might be: username1@username2@ip2
             let username = urlencoding::encode(username);
             result.push_str(&username);
             result.push('@');
         }
-        result.push_str(&self.host);
-        if let Some(port) = self.port {
-            result.push(':');
-            result.push_str(&port.to_string());
-        }
+
+        result.push_str(&self.host.to_string());
         result
     }
 
@@ -1264,6 +1427,11 @@ impl SshConnectionOptions {
             args.extend(["-o".to_string(), format!("ConnectTimeout={}", timeout)]);
         }
 
+        if let Some(port) = self.port {
+            args.push("-p".to_string());
+            args.push(port.to_string());
+        }
+
         if let Some(forwards) = &self.port_forwards {
             args.extend(forwards.iter().map(|pf| {
                 let local_host = match &pf.local_host {
@@ -1285,22 +1453,23 @@ impl SshConnectionOptions {
         args
     }
 
-    fn scp_url(&self) -> String {
+    fn scp_destination(&self) -> String {
         if let Some(username) = &self.username {
-            format!("{}@{}", username, self.host)
+            format!("{}@{}", username, self.host.to_bracketed_string())
         } else {
-            self.host.clone()
+            self.host.to_string()
         }
     }
 
     pub fn connection_string(&self) -> String {
-        let host = if let Some(username) = &self.username {
-            format!("{}@{}", username, self.host)
+        let host = if let Some(port) = &self.port {
+            format!("{}:{}", self.host.to_bracketed_string(), port)
         } else {
-            self.host.clone()
+            self.host.to_string()
         };
-        if let Some(port) = &self.port {
-            format!("{}:{}", host, port)
+
+        if let Some(username) = &self.username {
+            format!("{}@{}", username, host)
         } else {
             host
         }
@@ -1510,4 +1679,44 @@ mod tests {
             ]
         );
     }
+
+    #[test]
+    fn test_host_parsing() -> Result<()> {
+        let opts = SshConnectionOptions::parse_command_line("user@2001:db8::1")?;
+        assert_eq!(opts.host, "2001:db8::1".into());
+        assert_eq!(opts.username, Some("user".to_string()));
+        assert_eq!(opts.port, None);
+
+        let opts = SshConnectionOptions::parse_command_line("user@[2001:db8::1]:2222")?;
+        assert_eq!(opts.host, "2001:db8::1".into());
+        assert_eq!(opts.username, Some("user".to_string()));
+        assert_eq!(opts.port, Some(2222));
+
+        let opts = SshConnectionOptions::parse_command_line("user@[2001:db8::1]")?;
+        assert_eq!(opts.host, "2001:db8::1".into());
+        assert_eq!(opts.username, Some("user".to_string()));
+        assert_eq!(opts.port, None);
+
+        let opts = SshConnectionOptions::parse_command_line("2001:db8::1")?;
+        assert_eq!(opts.host, "2001:db8::1".into());
+        assert_eq!(opts.username, None);
+        assert_eq!(opts.port, None);
+
+        let opts = SshConnectionOptions::parse_command_line("[2001:db8::1]:2222")?;
+        assert_eq!(opts.host, "2001:db8::1".into());
+        assert_eq!(opts.username, None);
+        assert_eq!(opts.port, Some(2222));
+
+        let opts = SshConnectionOptions::parse_command_line("user@example.com:2222")?;
+        assert_eq!(opts.host, "example.com".into());
+        assert_eq!(opts.username, Some("user".to_string()));
+        assert_eq!(opts.port, Some(2222));
+
+        let opts = SshConnectionOptions::parse_command_line("user@192.168.1.1:2222")?;
+        assert_eq!(opts.host, "192.168.1.1".into());
+        assert_eq!(opts.username, Some("user".to_string()));
+        assert_eq!(opts.port, Some(2222));
+
+        Ok(())
+    }
 }

crates/remote/src/transport/wsl.rs πŸ”—

@@ -1,5 +1,5 @@
 use crate::{
-    RemoteClientDelegate, RemotePlatform,
+    RemoteArch, RemoteClientDelegate, RemoteOs, RemotePlatform,
     remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions},
     transport::{parse_platform, parse_shell},
 };
@@ -70,7 +70,10 @@ impl WslRemoteConnection {
         let mut this = Self {
             connection_options,
             remote_binary_path: None,
-            platform: RemotePlatform { os: "", arch: "" },
+            platform: RemotePlatform {
+                os: RemoteOs::Linux,
+                arch: RemoteArch::X86_64,
+            },
             shell: String::new(),
             shell_kind: ShellKind::Posix,
             default_system_shell: String::from("/bin/sh"),

crates/remote_server/src/headless_project.rs πŸ”—

@@ -642,16 +642,13 @@ impl HeadlessProject {
             .update(|cx| TrustedWorktrees::try_get_global(cx))?
             .context("missing trusted worktrees")?;
         trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
-            let mut restricted_paths = envelope
+            let restricted_paths = envelope
                 .payload
                 .worktree_ids
                 .into_iter()
                 .map(WorktreeId::from_proto)
                 .map(PathTrust::Worktree)
                 .collect::<HashSet<_>>();
-            if envelope.payload.restrict_workspace {
-                restricted_paths.insert(PathTrust::Workspace);
-            }
             trusted_worktrees.restrict(restricted_paths, None, cx);
         })?;
         Ok(proto::Ack {})

crates/remote_server/src/unix.rs πŸ”—

@@ -2,6 +2,7 @@ use crate::HeadlessProject;
 use crate::headless_project::HeadlessAppState;
 use anyhow::{Context as _, Result, anyhow};
 use client::ProxySettings;
+use collections::HashMap;
 use project::trusted_worktrees;
 use util::ResultExt;
 
@@ -35,7 +36,6 @@ use smol::Async;
 use smol::channel::{Receiver, Sender};
 use smol::io::AsyncReadExt;
 use smol::{net::unix::UnixListener, stream::StreamExt as _};
-use std::pin::Pin;
 use std::{
     env,
     ffi::OsStr,
@@ -419,7 +419,7 @@ pub fn execute_run(
 
         log::info!("gpui app started, initializing server");
         let session = start_server(listeners, log_rx, cx, is_wsl_interop);
-        trusted_worktrees::init(Some((session.clone(), REMOTE_SERVER_PROJECT_ID)), None, cx);
+        trusted_worktrees::init(HashMap::default(), Some((session.clone(), REMOTE_SERVER_PROJECT_ID)), None, cx);
 
         GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
         git_hosting_providers::init(cx);
@@ -452,13 +452,10 @@ pub fn execute_run(
                 )
             };
 
-            let trust_task = trusted_worktrees::wait_for_default_workspace_trust("Node runtime", cx)
-                .map(|trust_task| Box::pin(trust_task) as Pin<Box<_>>);
             let node_runtime = NodeRuntime::new(
                 http_client.clone(),
                 Some(shell_env_loaded_rx),
                 node_settings_rx,
-                trust_task,
             );
 
             let mut languages = LanguageRegistry::new(cx.background_executor().clone());

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::{
-    Action, App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable,
-    PromptLevel, Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle,
-    WindowOptions, actions, point, size, transparent_black,
+    App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel,
+    Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions,
+    actions, point, size, transparent_black,
 };
 use language::{Buffer, LanguageRegistry, language_settings::SoftWrap};
 use language_model::{
@@ -21,7 +21,7 @@ use std::sync::atomic::AtomicBool;
 use std::time::Duration;
 use theme::ThemeSettings;
 use title_bar::platform_title_bar::PlatformTitleBar;
-use ui::{Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
+use ui::{Divider, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
 use util::{ResultExt, TryFutureExt};
 use workspace::{Workspace, WorkspaceSettings, client_side_decorations};
 use zed_actions::assistant::InlineAssist;
@@ -206,13 +206,8 @@ impl PickerDelegate for RulePickerDelegate {
         self.filtered_entries.len()
     }
 
-    fn no_matches_text(&self, _window: &mut Window, cx: &mut App) -> Option<SharedString> {
-        let text = if self.store.read(cx).prompt_count() == 0 {
-            "No rules.".into()
-        } else {
-            "No rules found matching your search.".into()
-        };
-        Some(text)
+    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
+        Some("No rules found matching your search.".into())
     }
 
     fn selected_index(&self) -> usize {
@@ -680,13 +675,13 @@ impl RulesLibrary {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Some(default_content) = prompt_id.default_content() else {
+        let Some(built_in) = prompt_id.as_built_in() else {
             return;
         };
 
         if let Some(rule_editor) = self.rule_editors.get(&prompt_id) {
             rule_editor.body_editor.update(cx, |editor, cx| {
-                editor.set_text(default_content, window, cx);
+                editor.set_text(built_in.default_content(), window, cx);
             });
         }
     }
@@ -720,7 +715,7 @@ impl RulesLibrary {
             if focus {
                 rule_editor
                     .body_editor
-                    .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)));
+                    .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx));
             }
             self.set_active_rule(Some(prompt_id), window, cx);
         } else if let Some(rule_metadata) = self.store.read(cx).metadata(prompt_id) {
@@ -763,7 +758,7 @@ impl RulesLibrary {
                             editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
                             editor.set_completion_provider(Some(make_completion_provider()));
                             if focus {
-                                window.focus(&editor.focus_handle(cx));
+                                window.focus(&editor.focus_handle(cx), cx);
                             }
                             editor
                         });
@@ -939,7 +934,7 @@ impl RulesLibrary {
         if let Some(active_rule) = self.active_rule_id {
             self.rule_editors[&active_rule]
                 .body_editor
-                .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)));
+                .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx));
             cx.stop_propagation();
         }
     }
@@ -998,7 +993,7 @@ impl RulesLibrary {
         if let Some(rule_id) = self.active_rule_id
             && let Some(rule_editor) = self.rule_editors.get(&rule_id)
         {
-            window.focus(&rule_editor.body_editor.focus_handle(cx));
+            window.focus(&rule_editor.body_editor.focus_handle(cx), cx);
         }
     }
 
@@ -1011,7 +1006,7 @@ impl RulesLibrary {
         if let Some(rule_id) = self.active_rule_id
             && let Some(rule_editor) = self.rule_editors.get(&rule_id)
         {
-            window.focus(&rule_editor.title_editor.focus_handle(cx));
+            window.focus(&rule_editor.title_editor.focus_handle(cx), cx);
         }
     }
 
@@ -1308,8 +1303,8 @@ impl RulesLibrary {
                         .size_full()
                         .relative()
                         .overflow_hidden()
-                        .on_click(cx.listener(move |_, _, window, _| {
-                            window.focus(&focus_handle);
+                        .on_click(cx.listener(move |_, _, window, cx| {
+                            window.focus(&focus_handle, cx);
                         }))
                         .child(
                             h_flex()
@@ -1428,31 +1423,7 @@ impl Render for RulesLibrary {
                             this.border_t_1().border_color(cx.theme().colors().border)
                         })
                         .child(self.render_rule_list(cx))
-                        .map(|el| {
-                            if self.store.read(cx).prompt_count() == 0 {
-                                el.child(
-                                    v_flex()
-                                        .h_full()
-                                        .flex_1()
-                                        .items_center()
-                                        .justify_center()
-                                        .border_l_1()
-                                        .border_color(cx.theme().colors().border)
-                                        .bg(cx.theme().colors().editor_background)
-                                        .child(
-                                            Button::new("create-rule", "New Rule")
-                                                .style(ButtonStyle::Outlined)
-                                                .key_binding(KeyBinding::for_action(&NewRule, cx))
-                                                .on_click(|_, window, cx| {
-                                                    window
-                                                        .dispatch_action(NewRule.boxed_clone(), cx)
-                                                }),
-                                        ),
-                                )
-                            } else {
-                                el.child(self.render_active_rule(cx))
-                            }
-                        }),
+                        .child(self.render_active_rule(cx)),
                 ),
             window,
             cx,

crates/search/src/buffer_search.rs πŸ”—

@@ -7,7 +7,6 @@ use crate::{
     search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input},
 };
 use any_vec::AnyVec;
-use anyhow::Context as _;
 use collections::HashMap;
 use editor::{
     DisplayPoint, Editor, EditorSettings, MultiBufferOffset,
@@ -518,7 +517,7 @@ impl BufferSearchBar {
 
     pub fn register(registrar: &mut impl SearchActionsRegistrar) {
         registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| {
-            this.query_editor.focus_handle(cx).focus(window);
+            this.query_editor.focus_handle(cx).focus(window, cx);
             this.select_query(window, cx);
         }));
         registrar.register_handler(ForDeployed(
@@ -634,15 +633,19 @@ impl BufferSearchBar {
                 .read(cx)
                 .as_singleton()
                 .expect("query editor should be backed by a singleton buffer");
+
             query_buffer
                 .read(cx)
                 .set_language_registry(languages.clone());
 
             cx.spawn(async move |buffer_search_bar, cx| {
+                use anyhow::Context as _;
+
                 let regex_language = languages
                     .language_for_name("regex")
                     .await
                     .context("loading regex language")?;
+
                 buffer_search_bar
                     .update(cx, |buffer_search_bar, cx| {
                         buffer_search_bar.regex_language = Some(regex_language);
@@ -706,7 +709,7 @@ impl BufferSearchBar {
             active_editor.search_bar_visibility_changed(false, window, cx);
             active_editor.toggle_filtered_search_ranges(None, window, cx);
             let handle = active_editor.item_focus_handle(cx);
-            self.focus(&handle, window);
+            self.focus(&handle, window, cx);
         }
 
         cx.emit(Event::UpdateLocation);
@@ -749,7 +752,7 @@ impl BufferSearchBar {
                     self.select_query(window, cx);
                 }
 
-                window.focus(&handle);
+                window.focus(&handle, cx);
             }
             return true;
         }
@@ -878,7 +881,7 @@ impl BufferSearchBar {
     }
 
     pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        self.focus(&self.replacement_editor.focus_handle(cx), window);
+        self.focus(&self.replacement_editor.focus_handle(cx), window, cx);
         cx.notify();
     }
 
@@ -909,7 +912,7 @@ impl BufferSearchBar {
     pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context<Self>) {
         if let Some(active_editor) = self.active_searchable_item.as_ref() {
             let handle = active_editor.item_focus_handle(cx);
-            window.focus(&handle);
+            window.focus(&handle, cx);
         }
     }
 
@@ -1384,7 +1387,7 @@ impl BufferSearchBar {
             Direction::Prev => (current_index - 1) % handles.len(),
         };
         let next_focus_handle = &handles[new_index];
-        self.focus(next_focus_handle, window);
+        self.focus(next_focus_handle, window, cx);
         cx.stop_propagation();
     }
 
@@ -1431,9 +1434,9 @@ impl BufferSearchBar {
         }
     }
 
-    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) {
+    fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window, cx: &mut App) {
         window.invalidate_character_coordinates();
-        window.focus(handle);
+        window.focus(handle, cx);
     }
 
     fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context<Self>) {
@@ -1444,7 +1447,7 @@ impl BufferSearchBar {
             } else {
                 self.query_editor.focus_handle(cx)
             };
-            self.focus(&handle, window);
+            self.focus(&handle, window, cx);
             cx.notify();
         }
     }
@@ -2038,7 +2041,7 @@ mod tests {
             .update(cx, |_, window, cx| {
                 search_bar.update(cx, |search_bar, cx| {
                     let handle = search_bar.query_editor.focus_handle(cx);
-                    window.focus(&handle);
+                    window.focus(&handle, cx);
                     search_bar.activate_current_match(window, cx);
                 });
                 assert!(
@@ -2056,7 +2059,7 @@ mod tests {
                 search_bar.update(cx, |search_bar, cx| {
                     assert_eq!(search_bar.active_match_index, Some(0));
                     let handle = search_bar.query_editor.focus_handle(cx);
-                    window.focus(&handle);
+                    window.focus(&handle, cx);
                     search_bar.select_all_matches(&SelectAllMatches, window, cx);
                 });
                 assert!(
@@ -2109,7 +2112,7 @@ mod tests {
                         "Match index should be updated to the next one"
                     );
                     let handle = search_bar.query_editor.focus_handle(cx);
-                    window.focus(&handle);
+                    window.focus(&handle, cx);
                     search_bar.select_all_matches(&SelectAllMatches, window, cx);
                 });
             })
@@ -2175,7 +2178,7 @@ mod tests {
             .update(cx, |_, window, cx| {
                 search_bar.update(cx, |search_bar, cx| {
                     let handle = search_bar.query_editor.focus_handle(cx);
-                    window.focus(&handle);
+                    window.focus(&handle, cx);
                     search_bar.search("abas_nonexistent_match", None, true, window, cx)
                 })
             })

crates/search/src/project_search.rs πŸ”—

@@ -954,9 +954,9 @@ impl ProjectSearchView {
             cx.on_next_frame(window, |this, window, cx| {
                 if this.focus_handle.is_focused(window) {
                     if this.has_matches() {
-                        this.results_editor.focus_handle(cx).focus(window);
+                        this.results_editor.focus_handle(cx).focus(window, cx);
                     } else {
-                        this.query_editor.focus_handle(cx).focus(window);
+                        this.query_editor.focus_handle(cx).focus(window, cx);
                     }
                 }
             });
@@ -1453,7 +1453,7 @@ impl ProjectSearchView {
             query_editor.select_all(&SelectAll, window, cx);
         });
         let editor_handle = self.query_editor.focus_handle(cx);
-        window.focus(&editor_handle);
+        window.focus(&editor_handle, cx);
     }
 
     fn set_query(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
@@ -1493,7 +1493,7 @@ impl ProjectSearchView {
             });
         });
         let results_handle = self.results_editor.focus_handle(cx);
-        window.focus(&results_handle);
+        window.focus(&results_handle, cx);
     }
 
     fn entity_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -1750,7 +1750,7 @@ impl ProjectSearchBar {
     fn focus_search(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         if let Some(search_view) = self.active_project_search.as_ref() {
             search_view.update(cx, |search_view, cx| {
-                search_view.query_editor.focus_handle(cx).focus(window);
+                search_view.query_editor.focus_handle(cx).focus(window, cx);
             });
         }
     }
@@ -1783,7 +1783,7 @@ impl ProjectSearchBar {
                 Direction::Prev => (current_index - 1) % views.len(),
             };
             let next_focus_handle = &views[new_index];
-            window.focus(next_focus_handle);
+            window.focus(next_focus_handle, cx);
             cx.stop_propagation();
         });
     }
@@ -1832,7 +1832,7 @@ impl ProjectSearchBar {
                 } else {
                     this.query_editor.focus_handle(cx)
                 };
-                window.focus(&editor_to_focus);
+                window.focus(&editor_to_focus, cx);
                 cx.notify();
             });
         }
@@ -4352,7 +4352,7 @@ pub mod tests {
         let buffer_search_query = "search bar query";
         buffer_search_bar
             .update_in(&mut cx, |buffer_search_bar, window, cx| {
-                buffer_search_bar.focus_handle(cx).focus(window);
+                buffer_search_bar.focus_handle(cx).focus(window, cx);
                 buffer_search_bar.search(buffer_search_query, None, true, window, cx)
             })
             .await

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

@@ -143,7 +143,7 @@ impl SearchOption {
                 let focus_handle = focus_handle.clone();
                 button.on_click(move |_: &ClickEvent, window, cx| {
                     if !focus_handle.is_focused(window) {
-                        window.focus(&focus_handle);
+                        window.focus(&focus_handle, cx);
                     }
                     window.dispatch_action(action.boxed_clone(), cx);
                 })

crates/search/src/search_bar.rs πŸ”—

@@ -27,7 +27,7 @@ pub(super) fn render_action_button(
         let focus_handle = focus_handle.clone();
         move |_, window, cx| {
             if !focus_handle.is_focused(window) {
-                window.focus(&focus_handle);
+                window.focus(&focus_handle, cx);
             }
             window.dispatch_action(action.boxed_clone(), cx);
         }

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

@@ -158,6 +158,9 @@ pub struct SettingsContent {
     /// Default: false
     pub disable_ai: Option<SaturatingBool>,
 
+    /// Settings for the which-key popup.
+    pub which_key: Option<WhichKeySettingsContent>,
+
     /// Settings related to Vim mode in Zed.
     pub vim: Option<VimSettingsContent>,
 }
@@ -976,6 +979,19 @@ pub struct ReplSettingsContent {
     pub max_columns: Option<usize>,
 }
 
+/// Settings for configuring the which-key popup behaviour.
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
+pub struct WhichKeySettingsContent {
+    /// Whether to show the which-key popup when holding down key combinations
+    ///
+    /// Default: false
+    pub enabled: Option<bool>,
+    /// Delay in milliseconds before showing the which-key popup.
+    ///
+    /// Default: 700
+    pub delay_ms: Option<u64>,
+}
+
 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
 /// An ExtendingVec in the settings can only accumulate new values.
 ///

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

@@ -38,6 +38,9 @@ pub struct AgentSettingsContent {
     pub default_height: Option<f32>,
     /// The default model to use when creating new chats and for other features when a specific model is not specified.
     pub default_model: Option<LanguageModelSelection>,
+    /// Favorite models to show at the top of the model selector.
+    #[serde(default)]
+    pub favorite_models: Vec<LanguageModelSelection>,
     /// Model to use for the inline assistant. Defaults to default_model when not specified.
     pub inline_assistant_model: Option<LanguageModelSelection>,
     /// Model to use for the inline assistant when streaming tools are enabled.
@@ -176,6 +179,16 @@ impl AgentSettingsContent {
     pub fn set_profile(&mut self, profile_id: Arc<str>) {
         self.default_profile = Some(profile_id);
     }
+
+    pub fn add_favorite_model(&mut self, model: LanguageModelSelection) {
+        if !self.favorite_models.contains(&model) {
+            self.favorite_models.push(model);
+        }
+    }
+
+    pub fn remove_favorite_model(&mut self, model: &LanguageModelSelection) {
+        self.favorite_models.retain(|m| m != model);
+    }
 }
 
 #[with_fallible_options]

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

@@ -288,6 +288,11 @@ impl std::fmt::Debug for ContextServerCommand {
 #[with_fallible_options]
 #[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct GitSettings {
+    /// Whether or not to enable git integration.
+    ///
+    /// Default: true
+    #[serde(flatten)]
+    pub enabled: Option<GitEnabledSettings>,
     /// Whether or not to show the git gutter.
     ///
     /// Default: tracked_files
@@ -317,6 +322,25 @@ pub struct GitSettings {
     pub path_style: Option<GitPathStyle>,
 }
 
+#[with_fallible_options]
+#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
+#[serde(rename_all = "snake_case")]
+pub struct GitEnabledSettings {
+    pub disable_git: Option<bool>,
+    pub enable_status: Option<bool>,
+    pub enable_diff: Option<bool>,
+}
+
+impl GitEnabledSettings {
+    pub fn is_git_status_enabled(&self) -> bool {
+        !self.disable_git.unwrap_or(false) && self.enable_status.unwrap_or(true)
+    }
+
+    pub fn is_git_diff_enabled(&self) -> bool {
+        !self.disable_git.unwrap_or(false) && self.enable_diff.unwrap_or(true)
+    }
+}
+
 #[derive(
     Clone,
     Copy,

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

@@ -1233,6 +1233,49 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                             }
                         }).collect(),
                     }),
+                    SettingsPageItem::SectionHeader("Which-key Menu"),
+                    SettingsPageItem::SettingItem(SettingItem {
+                        title: "Show Which-key Menu",
+                        description: "Display the which-key menu with matching bindings while a multi-stroke binding is pending.",
+                        field: Box::new(SettingField {
+                            json_path: Some("which_key.enabled"),
+                            pick: |settings_content| {
+                                settings_content
+                                    .which_key
+                                    .as_ref()
+                                    .and_then(|settings| settings.enabled.as_ref())
+                            },
+                            write: |settings_content, value| {
+                                settings_content
+                                    .which_key
+                                    .get_or_insert_default()
+                                    .enabled = value;
+                            },
+                        }),
+                        metadata: None,
+                        files: USER,
+                    }),
+                    SettingsPageItem::SettingItem(SettingItem {
+                        title: "Menu Delay",
+                        description: "Delay in milliseconds before the which-key menu appears.",
+                        field: Box::new(SettingField {
+                            json_path: Some("which_key.delay_ms"),
+                            pick: |settings_content| {
+                                settings_content
+                                    .which_key
+                                    .as_ref()
+                                    .and_then(|settings| settings.delay_ms.as_ref())
+                            },
+                            write: |settings_content, value| {
+                                settings_content
+                                    .which_key
+                                    .get_or_insert_default()
+                                    .delay_ms = value;
+                            },
+                        }),
+                        metadata: None,
+                        files: USER,
+                    }),
                     SettingsPageItem::SectionHeader("Multibuffer"),
                     SettingsPageItem::SettingItem(SettingItem {
                         title: "Double Click In Multibuffer",
@@ -5476,6 +5519,102 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
         SettingsPage {
             title: "Version Control",
             items: vec![
+                SettingsPageItem::SectionHeader("Git Integration"),
+                SettingsPageItem::DynamicItem(DynamicItem {
+                    discriminant: SettingItem {
+                        files: USER,
+                        title: "Disable Git Integration",
+                        description: "Disable all Git integration features in Zed.",
+                        field: Box::new(SettingField::<bool> {
+                            json_path: Some("git.disable_git"),
+                            pick: |settings_content| {
+                                settings_content
+                                    .git
+                                    .as_ref()?
+                                    .enabled
+                                    .as_ref()?
+                                    .disable_git
+                                    .as_ref()
+                            },
+                            write: |settings_content, value| {
+                                settings_content
+                                    .git
+                                    .get_or_insert_default()
+                                    .enabled
+                                    .get_or_insert_default()
+                                    .disable_git = value;
+                            },
+                        }),
+                        metadata: None,
+                    },
+                    pick_discriminant: |settings_content| {
+                        let disabled = settings_content
+                            .git
+                            .as_ref()?
+                            .enabled
+                            .as_ref()?
+                            .disable_git
+                            .unwrap_or(false);
+                        Some(if disabled { 0 } else { 1 })
+                    },
+                    fields: vec![
+                        vec![],
+                        vec![
+                            SettingItem {
+                                files: USER,
+                                title: "Enable Git Status",
+                                description: "Show Git status information in the editor.",
+                                field: Box::new(SettingField::<bool> {
+                                    json_path: Some("git.enable_status"),
+                                    pick: |settings_content| {
+                                        settings_content
+                                            .git
+                                            .as_ref()?
+                                            .enabled
+                                            .as_ref()?
+                                            .enable_status
+                                            .as_ref()
+                                    },
+                                    write: |settings_content, value| {
+                                        settings_content
+                                            .git
+                                            .get_or_insert_default()
+                                            .enabled
+                                            .get_or_insert_default()
+                                            .enable_status = value;
+                                    },
+                                }),
+                                metadata: None,
+                            },
+                            SettingItem {
+                                files: USER,
+                                title: "Enable Git Diff",
+                                description: "Show Git diff information in the editor.",
+                                field: Box::new(SettingField::<bool> {
+                                    json_path: Some("git.enable_diff"),
+                                    pick: |settings_content| {
+                                        settings_content
+                                            .git
+                                            .as_ref()?
+                                            .enabled
+                                            .as_ref()?
+                                            .enable_diff
+                                            .as_ref()
+                                    },
+                                    write: |settings_content, value| {
+                                        settings_content
+                                            .git
+                                            .get_or_insert_default()
+                                            .enabled
+                                            .get_or_insert_default()
+                                            .enable_diff = value;
+                                    },
+                                }),
+                                metadata: None,
+                            },
+                        ],
+                    ],
+                }),
                 SettingsPageItem::SectionHeader("Git Gutter"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Visibility",

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

@@ -345,8 +345,8 @@ impl NonFocusableHandle {
     fn from_handle(handle: FocusHandle, window: &mut Window, cx: &mut App) -> Entity<Self> {
         cx.new(|cx| {
             let _subscription = cx.on_focus(&handle, window, {
-                move |_, window, _| {
-                    window.focus_next();
+                move |_, window, cx| {
+                    window.focus_next(cx);
                 }
             });
             Self {
@@ -890,7 +890,7 @@ impl SettingsPageItem {
                             .size(ButtonSize::Medium)
                             .on_click({
                                 let sub_page_link = sub_page_link.clone();
-                                cx.listener(move |this, _, _, cx| {
+                                cx.listener(move |this, _, window, cx| {
                                     let mut section_index = item_index;
                                     let current_page = this.current_page();
 
@@ -909,7 +909,7 @@ impl SettingsPageItem {
                                         )
                                     };
 
-                                    this.push_sub_page(sub_page_link.clone(), header, cx)
+                                    this.push_sub_page(sub_page_link.clone(), header, window, cx)
                                 })
                             }),
                         )
@@ -1537,7 +1537,7 @@ impl SettingsWindow {
         this.build_search_index();
 
         this.search_bar.update(cx, |editor, cx| {
-            editor.focus_handle(cx).focus(window);
+            editor.focus_handle(cx).focus(window, cx);
         });
 
         this
@@ -2174,7 +2174,7 @@ impl SettingsWindow {
                     let focus_handle = focus_handle.clone();
                     move |this, _: &gpui::ClickEvent, window, cx| {
                         this.change_file(ix, window, cx);
-                        focus_handle.focus(window);
+                        focus_handle.focus(window, cx);
                     }
                 }))
             };
@@ -2251,7 +2251,7 @@ impl SettingsWindow {
                                                             this.update(cx, |this, cx| {
                                                                 this.change_file(ix, window, cx);
                                                             });
-                                                            focus_handle.focus(window);
+                                                            focus_handle.focus(window, cx);
                                                         }
                                                     },
                                                 );
@@ -2385,7 +2385,7 @@ impl SettingsWindow {
                 let focused_entry_parent = this.root_entry_containing(focused_entry);
                 if this.navbar_entries[focused_entry_parent].expanded {
                     this.toggle_navbar_entry(focused_entry_parent);
-                    window.focus(&this.navbar_entries[focused_entry_parent].focus_handle);
+                    window.focus(&this.navbar_entries[focused_entry_parent].focus_handle, cx);
                 }
                 cx.notify();
             }))
@@ -2534,6 +2534,7 @@ impl SettingsWindow {
                                                         window.focus(
                                                             &this.navbar_entries[entry_index]
                                                                 .focus_handle,
+                                                            cx,
                                                         );
                                                         cx.notify();
                                                     },
@@ -2658,7 +2659,7 @@ impl SettingsWindow {
         // back to back.
         cx.on_next_frame(window, move |_, window, cx| {
             if let Some(handle) = handle_to_focus.as_ref() {
-                window.focus(handle);
+                window.focus(handle, cx);
             }
 
             cx.on_next_frame(window, |_, _, cx| {
@@ -2725,7 +2726,7 @@ impl SettingsWindow {
         };
         self.navbar_scroll_handle
             .scroll_to_item(position, gpui::ScrollStrategy::Top);
-        window.focus(&self.navbar_entries[nav_entry_index].focus_handle);
+        window.focus(&self.navbar_entries[nav_entry_index].focus_handle, cx);
         cx.notify();
     }
 
@@ -2995,8 +2996,8 @@ impl SettingsWindow {
                             IconButton::new("back-btn", IconName::ArrowLeft)
                                 .icon_size(IconSize::Small)
                                 .shape(IconButtonShape::Square)
-                                .on_click(cx.listener(|this, _, _, cx| {
-                                    this.pop_sub_page(cx);
+                                .on_click(cx.listener(|this, _, window, cx| {
+                                    this.pop_sub_page(window, cx);
                                 })),
                         )
                         .child(self.render_sub_page_breadcrumbs()),
@@ -3100,7 +3101,7 @@ impl SettingsWindow {
             .id("settings-ui-page")
             .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| {
                 if !sub_page_stack().is_empty() {
-                    window.focus_next();
+                    window.focus_next(cx);
                     return;
                 }
                 for (logical_index, (actual_index, _)) in this.visible_page_items().enumerate() {
@@ -3120,7 +3121,7 @@ impl SettingsWindow {
                         cx.on_next_frame(window, |_, window, cx| {
                             cx.notify();
                             cx.on_next_frame(window, |_, window, cx| {
-                                window.focus_next();
+                                window.focus_next(cx);
                                 cx.notify();
                             });
                         });
@@ -3128,11 +3129,11 @@ impl SettingsWindow {
                         return;
                     }
                 }
-                window.focus_next();
+                window.focus_next(cx);
             }))
             .on_action(cx.listener(|this, _: &menu::SelectPrevious, window, cx| {
                 if !sub_page_stack().is_empty() {
-                    window.focus_prev();
+                    window.focus_prev(cx);
                     return;
                 }
                 let mut prev_was_header = false;
@@ -3152,7 +3153,7 @@ impl SettingsWindow {
                         cx.on_next_frame(window, |_, window, cx| {
                             cx.notify();
                             cx.on_next_frame(window, |_, window, cx| {
-                                window.focus_prev();
+                                window.focus_prev(cx);
                                 cx.notify();
                             });
                         });
@@ -3161,7 +3162,7 @@ impl SettingsWindow {
                     }
                     prev_was_header = is_header;
                 }
-                window.focus_prev();
+                window.focus_prev(cx);
             }))
             .when(sub_page_stack().is_empty(), |this| {
                 this.vertical_scrollbar_for(&self.list_state, window, cx)
@@ -3355,23 +3356,28 @@ impl SettingsWindow {
         &mut self,
         sub_page_link: SubPageLink,
         section_header: &'static str,
+        window: &mut Window,
         cx: &mut Context<SettingsWindow>,
     ) {
         sub_page_stack_mut().push(SubPage {
             link: sub_page_link,
             section_header,
         });
+        self.sub_page_scroll_handle
+            .set_offset(point(px(0.), px(0.)));
+        self.content_focus_handle.focus_handle(cx).focus(window, cx);
         cx.notify();
     }
 
-    fn pop_sub_page(&mut self, cx: &mut Context<SettingsWindow>) {
+    fn pop_sub_page(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
         sub_page_stack_mut().pop();
+        self.content_focus_handle.focus_handle(cx).focus(window, cx);
         cx.notify();
     }
 
-    fn focus_file_at_index(&mut self, index: usize, window: &mut Window) {
+    fn focus_file_at_index(&mut self, index: usize, window: &mut Window, cx: &mut App) {
         if let Some((_, handle)) = self.files.get(index) {
-            handle.focus(window);
+            handle.focus(window, cx);
         }
     }
 
@@ -3451,7 +3457,7 @@ impl Render for SettingsWindow {
                             window.minimize_window();
                         })
                         .on_action(cx.listener(|this, _: &search::FocusSearch, window, cx| {
-                            this.search_bar.focus_handle(cx).focus(window);
+                            this.search_bar.focus_handle(cx).focus(window, cx);
                         }))
                         .on_action(cx.listener(|this, _: &ToggleFocusNav, window, cx| {
                             if this
@@ -3471,8 +3477,8 @@ impl Render for SettingsWindow {
                             }
                         }))
                         .on_action(cx.listener(
-                            |this, FocusFile(file_index): &FocusFile, window, _| {
-                                this.focus_file_at_index(*file_index as usize, window);
+                            |this, FocusFile(file_index): &FocusFile, window, cx| {
+                                this.focus_file_at_index(*file_index as usize, window, cx);
                             },
                         ))
                         .on_action(cx.listener(|this, _: &FocusNextFile, window, cx| {
@@ -3480,11 +3486,11 @@ impl Render for SettingsWindow {
                                 this.focused_file_index(window, cx) + 1,
                                 this.files.len().saturating_sub(1),
                             );
-                            this.focus_file_at_index(next_index, window);
+                            this.focus_file_at_index(next_index, window, cx);
                         }))
                         .on_action(cx.listener(|this, _: &FocusPreviousFile, window, cx| {
                             let prev_index = this.focused_file_index(window, cx).saturating_sub(1);
-                            this.focus_file_at_index(prev_index, window);
+                            this.focus_file_at_index(prev_index, window, cx);
                         }))
                         .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| {
                             if this
@@ -3494,11 +3500,11 @@ impl Render for SettingsWindow {
                             {
                                 this.focus_and_scroll_to_first_visible_nav_entry(window, cx);
                             } else {
-                                window.focus_next();
+                                window.focus_next(cx);
                             }
                         }))
-                        .on_action(|_: &menu::SelectPrevious, window, _| {
-                            window.focus_prev();
+                        .on_action(|_: &menu::SelectPrevious, window, cx| {
+                            window.focus_prev(cx);
                         })
                         .flex()
                         .flex_row()

crates/supermaven/src/supermaven_edit_prediction_delegate.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{Supermaven, SupermavenCompletionStateId};
 use anyhow::Result;
-use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
+use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
 use futures::StreamExt as _;
 use gpui::{App, Context, Entity, EntityId, Task};
 use language::{Anchor, Buffer, BufferSnapshot};
@@ -189,15 +189,6 @@ impl EditPredictionDelegate for SupermavenEditPredictionDelegate {
         }));
     }
 
-    fn cycle(
-        &mut self,
-        _buffer: Entity<Buffer>,
-        _cursor_position: Anchor,
-        _direction: Direction,
-        _cx: &mut Context<Self>,
-    ) {
-    }
-
     fn accept(&mut self, _cx: &mut Context<Self>) {
         reset_completion_cache(self, _cx);
     }

crates/terminal/src/terminal_hyperlinks.rs πŸ”—

@@ -11,6 +11,7 @@ use alacritty_terminal::{
 use log::{info, warn};
 use regex::Regex;
 use std::{
+    iter::{once, once_with},
     ops::{Index, Range},
     time::{Duration, Instant},
 };
@@ -232,14 +233,17 @@ fn path_match<T>(
         (line_end.line.0 - line_start.line.0 + 1) as usize * term.grid().columns(),
     );
     let first_cell = &term.grid()[line_start];
+    let mut prev_len = 0;
     line.push(first_cell.c);
-    let mut start_offset = 0;
+    let mut prev_char_is_space = first_cell.c == ' ';
     let mut hovered_point_byte_offset = None;
+    let mut hovered_word_start_offset = None;
+    let mut hovered_word_end_offset = None;
 
-    if !first_cell.flags.intersects(WIDE_CHAR_SPACERS) {
-        start_offset += first_cell.c.len_utf8();
-        if line_start == hovered {
-            hovered_point_byte_offset = Some(0);
+    if line_start == hovered {
+        hovered_point_byte_offset = Some(0);
+        if first_cell.c != ' ' {
+            hovered_word_start_offset = Some(0);
         }
     }
 
@@ -247,27 +251,44 @@ fn path_match<T>(
         if cell.point > line_end {
             break;
         }
-        let is_spacer = cell.flags.intersects(WIDE_CHAR_SPACERS);
-        if cell.point == hovered {
-            debug_assert!(hovered_point_byte_offset.is_none());
-            if start_offset > 0 && cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
-                // If we hovered on a trailing spacer, back up to the end of the previous char's bytes.
-                start_offset -= 1;
+
+        if !cell.flags.intersects(WIDE_CHAR_SPACERS) {
+            prev_len = line.len();
+            match cell.c {
+                ' ' | '\t' => {
+                    if hovered_point_byte_offset.is_some() && !prev_char_is_space {
+                        if hovered_word_end_offset.is_none() {
+                            hovered_word_end_offset = Some(line.len());
+                        }
+                    }
+                    line.push(' ');
+                    prev_char_is_space = true;
+                }
+                c @ _ => {
+                    if hovered_point_byte_offset.is_none() && prev_char_is_space {
+                        hovered_word_start_offset = Some(line.len());
+                    }
+                    line.push(c);
+                    prev_char_is_space = false;
+                }
             }
-            hovered_point_byte_offset = Some(start_offset);
-        } else if cell.point < hovered && !is_spacer {
-            start_offset += cell.c.len_utf8();
         }
 
-        if !is_spacer {
-            line.push(match cell.c {
-                '\t' => ' ',
-                c @ _ => c,
-            });
+        if cell.point == hovered {
+            debug_assert!(hovered_point_byte_offset.is_none());
+            hovered_point_byte_offset = Some(prev_len);
         }
     }
     let line = line.trim_ascii_end();
     let hovered_point_byte_offset = hovered_point_byte_offset?;
+    let hovered_word_range = {
+        let word_start_offset = hovered_word_start_offset.unwrap_or(0);
+        (word_start_offset != 0)
+            .then_some(word_start_offset..hovered_word_end_offset.unwrap_or(line.len()))
+    };
+    if line.len() <= hovered_point_byte_offset {
+        return None;
+    }
     let found_from_range = |path_range: Range<usize>,
                             link_range: Range<usize>,
                             position: Option<(u32, Option<u32>)>| {
@@ -313,10 +334,27 @@ fn path_match<T>(
     for regex in path_hyperlink_regexes {
         let mut path_found = false;
 
-        for captures in regex.captures_iter(&line) {
+        for (line_start_offset, captures) in once(
+            regex
+                .captures_iter(&line)
+                .next()
+                .map(|captures| (0, captures)),
+        )
+        .chain(once_with(|| {
+            if let Some(hovered_word_range) = &hovered_word_range {
+                regex
+                    .captures_iter(&line[hovered_word_range.clone()])
+                    .next()
+                    .map(|captures| (hovered_word_range.start, captures))
+            } else {
+                None
+            }
+        }))
+        .flatten()
+        {
             path_found = true;
             let match_range = captures.get(0).unwrap().range();
-            let (path_range, line_column) = if let Some(path) = captures.name("path") {
+            let (mut path_range, line_column) = if let Some(path) = captures.name("path") {
                 let parse = |name: &str| {
                     captures
                         .name(name)
@@ -330,10 +368,15 @@ fn path_match<T>(
             } else {
                 (match_range.clone(), None)
             };
-            let link_range = captures
+            let mut link_range = captures
                 .name("link")
                 .map_or_else(|| match_range.clone(), |link| link.range());
 
+            path_range.start += line_start_offset;
+            path_range.end += line_start_offset;
+            link_range.start += line_start_offset;
+            link_range.end += line_start_offset;
+
             if !link_range.contains(&hovered_point_byte_offset) {
                 // No match, just skip.
                 continue;
@@ -638,9 +681,6 @@ mod tests {
             test_path!(
                 "β€ΉΒ«πŸ¦€ multiple_πŸ‘‰same_line πŸ¦€Β» 🚣«4Β» πŸ›οΈΒ«2Β»β€Ί: πŸ¦€ multiple_same_line πŸ¦€ 🚣4 πŸ›οΈ2:"
             );
-            test_path!(
-                "πŸ¦€ multiple_same_line πŸ¦€ 🚣4 πŸ›οΈ2 β€ΉΒ«πŸ¦€ multiple_πŸ‘‰same_line πŸ¦€Β» 🚣«4Β» πŸ›οΈΒ«2Β»β€Ί:"
-            );
 
             // ls output (tab separated)
             test_path!(
@@ -977,7 +1017,7 @@ mod tests {
             use crate::TerminalSettings;
             use alacritty_terminal::{
                 event::VoidListener,
-                grid::Dimensions,
+                grid::Scroll,
                 index::{Column, Point as AlacPoint},
                 term::test::mock_term,
                 term::{Term, search::Match},
@@ -986,14 +1026,20 @@ mod tests {
             use std::{cell::RefCell, rc::Rc};
             use util_macros::perf;
 
-            fn build_test_term(line: &str) -> (Term<VoidListener>, AlacPoint) {
-                let content = line.repeat(500);
-                let term = mock_term(&content);
-                let point = AlacPoint::new(
-                    term.grid().bottommost_line() - 1,
-                    Column(term.grid().last_column().0 / 2),
-                );
-
+            fn build_test_term(
+                line: &str,
+                repeat: usize,
+                hover_offset_column: usize,
+            ) -> (Term<VoidListener>, AlacPoint) {
+                let content = line.repeat(repeat);
+                let mut term = mock_term(&content);
+                term.resize(TermSize {
+                    columns: 1024,
+                    screen_lines: 10,
+                });
+                term.scroll_display(Scroll::Top);
+                let point =
+                    AlacPoint::new(Line(term.topmost_line().0 + 3), Column(hover_offset_column));
                 (term, point)
             }
 
@@ -1002,11 +1048,14 @@ mod tests {
                 const LINE: &str = "    Compiling terminal v0.1.0 (/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal)\r\n";
                 thread_local! {
                     static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
-                        build_test_term(LINE);
+                        build_test_term(LINE, 500, 50);
                 }
                 TEST_TERM_AND_POINT.with(|(term, point)| {
-                    assert!(
-                        find_from_grid_point_bench(term, *point).is_some(),
+                    assert_eq!(
+                        find_from_grid_point_bench(term, *point)
+                            .map(|(path, ..)| path)
+                            .unwrap_or_default(),
+                        "/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal",
                         "Hyperlink should have been found"
                     );
                 });
@@ -1017,11 +1066,14 @@ mod tests {
                 const LINE: &str = "    --> /Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42\r\n";
                 thread_local! {
                     static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
-                        build_test_term(LINE);
+                        build_test_term(LINE, 500, 50);
                 }
                 TEST_TERM_AND_POINT.with(|(term, point)| {
-                    assert!(
-                        find_from_grid_point_bench(term, *point).is_some(),
+                    assert_eq!(
+                        find_from_grid_point_bench(term, *point)
+                            .map(|(path, ..)| path)
+                            .unwrap_or_default(),
+                        "/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42",
                         "Hyperlink should have been found"
                     );
                 });
@@ -1032,11 +1084,111 @@ mod tests {
                 const LINE: &str = "Cargo.toml        experiments        notebooks        rust-toolchain.toml    tooling\r\n";
                 thread_local! {
                     static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
-                        build_test_term(LINE);
+                        build_test_term(LINE, 500, 60);
                 }
                 TEST_TERM_AND_POINT.with(|(term, point)| {
-                    assert!(
-                        find_from_grid_point_bench(term, *point).is_some(),
+                    assert_eq!(
+                        find_from_grid_point_bench(term, *point)
+                            .map(|(path, ..)| path)
+                            .unwrap_or_default(),
+                        "rust-toolchain.toml",
+                        "Hyperlink should have been found"
+                    );
+                });
+            }
+
+            #[perf]
+            // https://github.com/zed-industries/zed/pull/44407
+            pub fn pr_44407_hyperlink_benchmark() {
+                const LINE: &str = "-748, 706, 163, 222, -980, 949, 381, -568, 199, 501, 760, -821, 90, -451, 183, 867, -351, -810, -762, -109, 423, 84, 14, -77, -820, -345, 74, -791, 930, -618, -900, 862, -959, 289, -19, 471, -757, 793, 155, -554, 249, 830, 402, 732, -731, -866, -720, -703, -257, -439, 731, 872, -489, 676, -167, 613, -698, 415, -80, -453, -896, 333, -511, 621, -450, 624, -309, -575, 177, 141, 891, -104, -97, -367, -599, -675, 607, -225, -760, 552, -465, 804, 55, 282, 104, -929, -252,\
+-311, 900, 550, 599, -80, 774, 553, 837, -395, 541, 953, 154, -396, -596, -111, -802, -221, -337, -633, -73, -527, -82, -658, -264, 222, 375, 434, 204, -756, -703, 303, 239, -257, -365, -351, 904, 364, -743, -484, 655, -542, 446, 888, 632, -167, -260, 716, 150, 806, 723, 513, -118, -323, -683, 983, -564, 358, -16, -287, 277, -607, 87, 365, -1, 164, 401, 257, 369, -893, 145, -969, 375, -53, 541, -408, -865, 753, 258, 337, -886, 593, -378, -528, 191, 204, 566, -61, -621, 769, 524, -628, 6,\
+249, 896, -785, -776, 321, -681, 604, -740, 886, 426, -480, -983, 23, -247, 125, -666, 913, 842, -460, -797, -483, -58, -565, -587, -206, 197, 715, 764, -97, 457, -149, -226, 261, 194, -390, 431, 180, -778, 829, -657, -668, 397, 859, 152, -178, 677, -18, 687, -247, 96, 466, -572, 478, 622, -143, -25, -471, 265, 335, 957, 152, -951, -647, 670, 57, 152, -115, 206, 87, 629, -798, -125, -725, -31, 844, 398, -876, 44, 963, -211, 518, -8, -103, -999, 948, 823, 149, -803, 769, -236, -683, 527,\
+-108, -36, 18, -437, 687, -305, -526, 972, -965, 276, 420, -259, -379, -142, -747, 600, -578, 197, 673, 890, 324, -931, 755, -765, -422, 785, -369, -110, -505, 532, -208, -438, 713, 110, 853, 996, -360, 823, 289, -699, 629, -661, 560, -329, -323, 439, 571, -537, 644, -84, 25, -536, -161, 112, 169, -922, -537, -734, -423, 37, 451, -149, 408, 18, -672, 206, -784, 444, 593, -241, 502, -259, -798, -352, -658, 712, -675, -734, 627, -620, 64, -554, 999, -537, -160, -641, 464, 894, 29, 322, 566,\
+-510, -749, 982, 204, 967, -261, -986, -136, 251, -598, 995, -831, 891, 22, 761, -783, -415, 125, 470, -919, -97, -668, 85, 205, -175, -550, 502, 652, -468, 798, 775, -216, 89, -433, -24, -621, 877, -126, 951, 809, 782, 156, -618, -841, -463, 19, -723, -904, 550, 263, 991, -758, -114, 446, -731, -623, -634, 462, 48, 851, 333, -846, 480, 892, -966, -910, -436, 317, -711, -341, -294, 124, 238, -214, -281, 467, -950, -342, 913, -90, -388, -573, 740, -883, -451, 493, -500, 863, 930, 127, 530,\
+-810, 540, 541, -664, -951, -227, -420, -476, -581, -534, 549, 253, 984, -985, -84, -521, 538, 484, -440, 371, 784, -306, -850, 530, -133, 251, -799, 446, -170, -243, -674, 769, 646, 778, -680, -714, -442, 804, 901, -774, 69, 307, -293, 755, 443, 224, -918, -771, 723, 40, 132, 568, -847, -47, 844, 69, 986, -293, -459, 313, 155, 331, 69, 280, -637, 569, 104, -119, -988, 252, 857, -590, 810, -891, 484, 566, -934, -587, -290, 566, 587, 489, 870, 280, 454, -252, 613, -701, -278, 195, -198,\
+683, 533, -372, 707, -152, 371, 866, 609, -5, -372, -30, -694, 552, 192, 452, -663, 350, -985, 10, 884, 813, -592, -331, -470, 711, -941, 928, 379, -339, 220, 999, 376, 507, 179, 916, 84, 104, 392, 192, 299, -860, 218, -698, -919, -452, 37, 850, 5, -874, 287, 123, -746, -575, 776, -909, 118, 903, -275, 450, -996, -591, -920, -850, 453, -896, 73, 83, -535, -20, 287, -765, 442, 808, 45, 445, 202, 917, -208, 783, 790, -534, 373, -129, 556, -757, -69, 459, -163, -59, 265, -563, -889, 635,\
+-583, -261, -790, 799, 826, 953, 85, 619, 334, 842, 672, -869, -4, -833, 315, 942, -524, 579, 926, 628, -404, 128, -629, 161, 568, -117, -526, 223, -876, 906, 176, -549, -317, 381, 375, -801, -416, 647, 335, 253, -386, -375, -254, 635, 352, 317, 398, -422, 111, 201, 220, 554, -972, 853, 378, 956, 942, -857, -289, -333, -180, 488, -814, -42, -595, 721, 39, 644, 721, -242, -44, 643, -457, -419, 560, -863, 974, 458, 222, -882, 526, -243, -318, -343, -707, -401, 117, 677, -489, 546, -903,\
+-960, -881, -684, 125, -928, -995, -692, -773, 647, -718, -862, -814, 671, 664, -130, -856, -674, 653, 711, 194, -685, -160, 138, -27, -128, -671, -242, 526, 494, -674, 424, -921, -778, 313, -237, 332, 913, 252, 808, -936, 289, 755, 52, -139, 57, -19, -827, -775, -561, -14, 107, -84, 622, -303, -747, 258, -942, 290, 211, -919, -207, 797, 95, 794, -830, -181, -788, 757, 75, -946, -949, -988, 152, 340, 732, 886, -891, -642, -666, 321, -910, 841, 632, 298, 55, -349, 498, 287, -711, 97, 305,\
+-974, -987, 790, -64, 605, -583, -821, 345, 887, -861, 548, 894, 288, 452, 556, -448, 813, 420, 545, 967, 127, -947, 19, -314, -607, -513, -851, 254, -290, -938, -783, -93, 474, 368, -485, -935, -539, 81, 404, -283, 779, 345, -164, 53, 563, -771, 911, -323, 522, -998, 315, 415, 460, 58, -541, -878, -152, -886, 201, -446, -810, 549, -142, -575, -632, 521, 549, 209, -681, 998, 798, -611, -919, -708, -4, 677, -172, 588, 750, -435, 508, 609, 498, -535, -691, -738, 85, 615, 705, 169, 425,\
+-669, -491, -783, 73, -847, 228, -981, -812, -229, 950, -904, 175, -438, 632, -556, 910, 173, 576, -751, -53, -169, 635, 607, -944, -13, -84, 105, -644, 984, 935, 259, -445, 620, -405, 832, 167, 114, 209, -181, -944, -496, 693, -473, 137, 38, -873, -334, -353, -57, 397, 944, 698, 811, -401, 712, -667, 905, 276, -653, 368, -543, -349, 414, 287, 894, 935, 461, 55, 741, -623, -660, -773, 617, 834, 278, -121, 52, 495, -855, -440, -210, -99, 279, -661, 540, 934, 540, 784, 895, 268, -503, 513,\
+-484, -352, 528, 341, -451, 885, -71, 799, -195, -885, -585, -233, 92, 453, 994, 464, 694, 190, -561, -116, 675, -775, -236, 556, -110, -465, 77, -781, 507, -960, -410, 229, -632, 717, 597, 429, 358, -430, -692, -825, 576, 571, 758, -891, 528, -267, 190, -869, 132, -811, 796, 750, -596, -681, 870, 360, 969, 860, -412, -567, 694, -86, -498, 38, -178, -583, -778, 412, 842, -586, 722, -192, 350, 363, 81, -677, -163, 564, 543, 671, 110, 314, 739, -552, -224, -644, 922, 685, 134, 613, 793,\
+-363, -244, -284, -257, -561, 418, 988, 333, 110, -966, 790, 927, 536, -620, -309, -358, 895, -867, -796, -357, 308, -740, 287, -732, -363, -969, 658, 711, 511, 256, 590, -574, 815, -845, -84, 546, -581, -71, -334, -890, 652, -959, 320, -236, 445, -851, 825, -756, -4, 877, 308, 573, -117, 293, 686, -483, 391, 342, -550, -982, 713, 886, 552, 474, -673, 283, -591, -383, 988, 435, -131, 708, -326, -884, 87, 680, -818, -408, -486, 813, -307, -799, 23, -497, 802, -146, -100, 541, 7, -493, 577,\
+50, -270, 672, 834, 111, -788, 247, 337, 628, -33, -964, -519, 683, 54, -703, 633, -127, -448, 759, -975, 696, 2, -870, -760, 67, 696, 306, 750, 615, 155, -933, -568, 399, 795, 164, -460, 205, 439, -526, -691, 35, -136, -481, -63, 73, -598, 748, 133, 874, -29, 4, -73, 472, 389, 962, 231, -328, 240, 149, 959, 46, -207, 72, -514, -608, 0, -14, 32, 374, -478, -806, 919, -729, -286, 652, 109, 509, -879, -979, -865, 584, -92, -346, -992, 781, 401, 575, 993, -746, -33, 684, -683, 750, -105,\
+-425, -508, -627, 27, 770, -45, 338, 921, -139, -392, -933, 634, 563, 224, -780, 921, 991, 737, 22, 64, 414, -249, -687, 869, 50, 759, -97, 515, 20, -775, -332, 957, 138, -542, -835, 591, -819, 363, -715, -146, -950, -641, -35, -435, -407, -548, -984, 383, -216, -559, 853, 4, -410, -319, -831, -459, -628, -819, -324, 755, 696, -192, 238, -234, -724, -445, 915, 302, -708, 484, 224, -641, 25, -771, 528, -106, -744, -588, 913, -554, -515, -239, -843, -812, -171, 721, 543, -269, 440, 151,\
+996, -723, -557, -522, -280, -514, -593, 208, 715, 404, 353, 270, -483, -785, 318, -313, 798, 638, 764, 748, -929, -827, -318, -56, 389, -546, -958, -398, 463, -700, 461, 311, -787, -488, 877, 456, 166, 535, -995, -189, -715, 244, 40, 484, 212, -329, -351, 638, -69, -446, -292, 801, -822, 490, -486, -185, 790, 370, -340, 401, -656, 584, 561, -749, 269, -19, -294, -111, 975, 874, -73, 851, 231, -331, -684, 460, 765, -654, -76, 10, 733, 520, 521, 416, -958, -202, -186, -167, 175, 343, -50,\
+673, -763, -854, -977, -17, -853, -122, -25, 180, 149, 268, 874, -816, -745, 747, -303, -959, 390, 509, 18, -66, 275, -277, 9, 837, -124, 989, -542, -649, -845, 894, 926, 997, -847, -809, -579, -96, -372, 766, 238, -251, 503, 559, 276, -281, -102, -735, 815, 109, 175, -10, 128, 543, -558, -707, 949, 996, -422, -506, 252, 702, -930, 552, -961, 584, -79, -177, 341, -275, 503, -21, 677, -545, 8, -956, -795, -870, -254, 170, -502, -880, 106, 174, 459, 603, -600, -963, 164, -136, -641, -309,\
+-380, -707, -727, -10, 727, 952, 997, -731, -133, 269, 287, 855, 716, -650, 479, 299, -839, -308, -782, 769, 545, 663, -536, -115, 904, -986, -258, -562, 582, 664, 408, -525, -889, 471, -370, -534, -220, 310, 766, 931, -193, -897, -192, -74, -365, -256, -359, -328, 658, -691, -431, 406, 699, 425, 713, -584, -45, -588, 289, 658, -290, -880, -987, -444, 371, 904, -155, 81, -278, -708, -189, -78, 655, 342, -998, -647, -734, -218, 726, 619, 663, 744, 518, 60, -409, 561, -727, -961, -306,\
+-147, -550, 240, -218, -393, 267, 724, 791, -548, 480, 180, -631, 825, -170, 107, 227, -691, 905, -909, 359, 227, 287, 909, 632, -89, -522, 80, -429, 37, 561, -732, -474, 565, -798, -460, 188, 507, -511, -654, 212, -314, -376, -997, -114, -708, 512, -848, 781, 126, -956, -298, 354, -400, -121, 510, 445, 926, 27, -708, 676, 248, 834, 542, 236, -105, -153, 102, 128, 96, -348, -626, 598, 8, 978, -589, -461, -38, 381, -232, -817, 467, 356, -151, -460, 429, -408, 425, 618, -611, -247, 819,\
+963, -160, 1000, 141, -647, -875, 108, 790, -127, 463, -37, -195, -542, 12, 845, -384, 770, -129, 315, 826, -942, 430, 146, -170, -583, -903, -489, 497, -559, -401, -29, -129, -411, 166, 942, -646, -862, -404, 785, 777, -111, -481, -738, 490, 741, -398, 846, -178, -509, -661, 748, 297, -658, -567, 531, 427, -201, -41, -808, -668, 782, -860, -324, 249, 835, -234, 116, 542, -201, 328, 675, 480, -906, 188, 445, 63, -525, 811, 277, 133, 779, -680, 950, -477, -306, -64, 552, -890, -956, 169,\
+442, 44, -169, -243, -242, 423, -884, -757, -403, 739, -350, 383, 429, 153, -702, -725, 51, 310, 857, -56, 538, 46, -311, 132, -620, -297, -124, 534, 884, -629, -117, 506, -837, -100, -27, -381, -735, 262, 843, 703, 260, -457, 834, 469, 9, 950, 59, 127, -820, 518, 64, -783, 659, -608, -676, 802, 30, 589, 246, -369, 361, 347, 534, -376, 68, 941, 709, 264, 384, 481, 628, 199, -568, -342, -337, 853, -804, -858, -169, -270, 641, -344, 112, 530, -773, -349, -135, -367, -350, -756, -911, 180,\
+-660, 116, -478, -265, -581, 510, 520, -986, 935, 219, 522, 744, 47, -145, 917, 638, 301, 296, 858, -721, 511, -816, 328, 473, 441, 697, -260, -673, -379, 893, 458, 154, 86, 905, 590, 231, -717, -179, 79, 272, -439, -192, 178, -200, 51, 717, -256, -358, -626, -518, -314, -825, -325, 588, 675, -892, -798, 448, -518, 603, -23, 668, -655, 845, -314, 783, -347, -496, 921, 893, -163, -748, -906, 11, -143, -64, 300, 336, 882, 646, 533, 676, -98, -148, -607, -952, -481, -959, -874, 764, 537,\
+736, -347, 646, -843, 966, -916, -718, -391, -648, 740, 755, 919, -608, 388, -655, 68, 201, 675, -855, 7, -503, 881, 760, 669, 831, 721, -564, -445, 217, 331, 970, 521, 486, -254, 25, -259, 336, -831, 252, -995, 908, -412, -240, 123, -478, 366, 264, -504, -843, 632, -288, 896, 301, 423, 185, 318, 380, 457, -450, -162, -313, 673, -963, 570, 433, -548, 107, -39, -142, -98, -884, -3, 599, -486, -926, 923, -82, 686, 290, 99, -382, -789, 16, 495, 570, 284, 474, -504, -201, -178, -1, 592, 52,\
+827, -540, -151, -991, 130, 353, -420, -467, -661, 417, -690, 942, 936, 814, -566, -251, -298, 341, -139, 786, 129, 525, -861, 680, 955, -245, -50, 331, 412, -38, -66, 611, -558, 392, -629, -471, -68, -535, 744, 495, 87, 558, 695, 260, -308, 215, -464, 239, -50, 193, -540, 184, -8, -194, 148, 898, -557, -21, 884, 644, -785, -689, -281, -737, 267, 50, 206, 292, 265, 380, -511, 310, 53, 375, -497, -40, 312, -606, -395, 142, 422, 662, -584, 72, 144, 40, -679, -593, 581, 689, -829, 442, 822,\
+977, -832, -134, -248, -207, 248, 29, 259, 189, 592, -834, -866, 102, 0, 340, 25, -354, -239, 420, -730, -992, -925, -314, 420, 914, 607, -296, -415, -30, 813, 866, 153, -90, 150, -81, 636, -392, -222, -835, 482, -631, -962, -413, -727, 280, 686, -382, 157, -404, -511, -432, 455, 58, 108, -408, 290, -829, -252, 113, 550, -935, 925, 422, 38, 789, 361, 487, -460, -769, -963, -285, 206, -799, -488, -233, 416, 143, -456, 753, 520, 599, 621, -168, 178, -841, 51, 952, 374, 166, -300, -576, 844,\
+-656, 90, 780, 371, 730, -896, -895, -386, -662, 467, -61, 130, -362, -675, -113, 135, -761, -55, 408, 822, 675, -347, 725, 114, 952, -510, -972, 390, -413, -277, -52, 315, -80, 401, -712, 147, -202, 84, 214, -178, 970, -571, -210, 525, -887, -863, 504, 192, 837, -594, 203, -876, -209, 305, -826, 377, 103, -928, -803, -956, 949, -868, -547, 824, -994, 516, 93, -524, -866, -890, -988, -501, 15, -6, 413, -825, 304, -818, -223, 525, 176, 610, 828, 391, 940, 540, -831, 650, 438, 589, 941, 57,\
+523, 126, 221, 860, -282, -262, -226, 764, 743, -640, 390, 384, -434, 608, -983, 566, -446, 618, 456, -176, -278, 215, 871, -180, 444, -931, -200, -781, 404, 881, 780, -782, 517, -739, -548, -811, 201, -95, -249, -228, 491, -299, 700, 964, -550, 108, 334, -653, 245, -293, -552, 350, -685, -415, -818, 216, -194, -255, 295, 249, 408, 351, 287, 379, 682, 231, -693, 902, -902, 574, 937, -708, -402, -460, 827, -268, 791, 343, -780, -150, -738, 920, -430, -88, -361, -588, -727, -47, -297, 662,\
+-840, -637, -635, 916, -857, 938, 132, -553, 391, -522, 640, 626, 690, 833, 867, -555, 577, 226, 686, -44, 0, -965, 651, -1, 909, 595, -646, 740, -821, -648, -962, 927, -193, 159, 490, 594, -189, 707, -884, 759, -278, -160, -566, -340, 19, 862, -440, 445, -598, 341, 664, -311, 309, -159, 19, -672, 705, -646, 976, 247, 686, -830, -27, -667, 81, 399, -423, -567, 945, 38, 51, 740, 621, 204, -199, -908, -593, 424, 250, -561, 695, 9, 520, 878, 120, -109, 42, -375, -635, -711, -687, 383, -278,\
+36, 970, 925, 864, 836, 309, 117, 89, 654, -387, 346, -53, 617, -164, -624, 184, -45, 852, 498, -513, 794, -682, -576, 13, -147, 285, -776, -886, -96, 483, 994, -188, 346, -629, -848, 738, 51, 128, -898, -753, -906, 270, -203, -577, 48, -243, -210, 666, 353, 636, -954, 862, 560, -944, -877, -137, 440, -945, -316, 274, -211, -435, 615, -635, -468, 744, 948, -589, 525, 757, -191, -431, 42, 451, -160, -827, -991, 324, 697, 342, -610, 894, -787, -384, 872, 734, 878, 70, -260, 57, 397, -518,\
+629, -510, -94, 207, 214, -625, 106, -882, -575, 908, -650, 723, -154, 45, 108, -69, -565, 927, -68, -351, 707, -282, 429, -889, -596, 848, 578, -492, 41, -822, -992, 168, -286, -780, 970, 597, -293, -12, 367, 708, -415, 194, -86, -390, 224, 69, -368, -674, 1000, -672, 356, -202, -169, 826, 476, -285, 29, -448, 545, 186, 319, 67, 705, 412, 225, -212, -351, -391, -783, -9, 875, -59, -159, -123, -151, -296, 871, -638, 359, 909, -945, 345, -16, -562, -363, -183, -625, -115, -571, -329, 514,\
+99, 263, 463, -39, 597, -652, -349, 246, 77, -127, -563, -879, -30, 756, 777, -865, 675, -813, -501, 871, -406, -627, 834, -609, -205, -812, 643, -204, 291, -251, -184, -584, -541, 410, -573, -600, 908, -871, -687, 296, -713, -139, -778, -790, 347, -52, -400, 407, -653, 670, 39, -856, 904, 433, 392, 590, -271, -144, -863, 443, 353, 468, -544, 486, -930, 458, -596, -890, 163, 822, 768, 980, -783, -792, 126, 386, 367, -264, 603, -61, 728, 160, -4, -837, 832, 591, 436, 518, 796, -622, -867,\
+-669, -947, 253, 100, -792, 841, 413, 833, -249, -550, 282, -825, 936, -348, 898, -451, -283, 818, -237, 630, 216, -499, -637, -511, 767, -396, 221, 958, -586, -920, 401, -313, -580, -145, -270, 118, 497, 426, -975, 480, -445, -150, -721, -929, 439, -893, 902, 960, -525, -793, 924, 563, 683, -727, -86, 309, 432, -762, -345, 371, -617, 149, -215, -228, 505, 593, -20, -292, 704, -999, 149, -104, 819, -414, -443, 517, -599, -5, 145, -24, -993, -283, 904, 174, -112, -276, -860, 44, -257,\
+-931, -821, -667, 540, 421, 485, 531, 407, 833, 431, -415, 878, 503, -901, 639, -608, 896, 860, 927, 424, 113, -808, -323, 729, 382, -922, 548, -791, -379, 207, 203, 559, 537, 137, 999, -913, -240, 942, 249, 616, 775, -4, 915, 855, -987, -234, -384, 948, -310, -542, 125, -289, -599, 967, -492, -349, -552, 562, -926, 632, -164, 217, -165, -496, 847, 684, -884, 457, -748, -745, -38, 93, 961, 934, 588, 366, -130, 851, -803, -811, -211, 428, 183, -469, 888, 596, -475, -899, -681, 508, 184,\
+921, 863, -610, -416, -119, -966, -686, 210, 733, 715, -889, -925, -434, -566, -455, 596, -514, 983, 755, -194, -802, -313, 91, -541, 808, -834, 243, -377, 256, 966, -402, -773, -308, -605, 266, 866, 118, -425, -531, 498, 666, 813, -267, 830, 69, -869, -496, 735, 28, 488, -645, -493, -689, 170, -940, 532, 844, -658, -617, 408, -200, 764, -665, 568, 342, 621, 908, 471, 280, 859, 709, 898, 81, -547, 406, 514, -595, 43, -824, -696, -746, -429, -59, -263, -813, 233, 279, -125, 687, -418,\
+-530, 409, 614, 803, -407, 78, -676, -39, -887, -141, -292, 270, -343, 400, 907, 588, 668, 899, 973, 103, -101, -11, 397, -16, 165, 705, -410, -585, 316, 391, -346, -336, 957, -118, -538, -441, -845, 121, 591, -359, -188, -362, -208, 27, -925, -157, -495, -177, -580, 9, 531, -752, 94, 107, 820, 769, -500, 852, 617, 145, 355, 34, -463, -265, -709, -111, -855, -405, 560, 470, 3, -177, -164, -249, 450, 662, 841, -689, -509, 987, -33, 769, 234, -2, 203, 780, 744, -895, 497, -432, -406, -264,\
+-71, 124, 778, -897, 495, 127, -76, 52, -768, 205, 464, -992, 801, -83, -806, 545, -316, 146, 772, 786, 289, -936, 145, -30, -722, -455, 270, 444, 427, -482, 383, -861, 36, 630, -404, 83, 864, 743, -351, -846, 315, -837, 357, -195, 450, -715, 227, -942, 740, -519, 476, 716, 713, 169, 492, -112, -49, -931, 866, 95, -725, 198, -50, -17, -660, 356, -142, -781, 53, 431, 720, 143, -416, 446, -497, 490, -96, 157, 239, 487, -337, -224, -445, 813, 92, -22, 603, 424, 952, -632, -367, 898, -927,\
+884, -277, -187, -777, 537, -575, -313, 347, -33, 800, 672, -919, -541, 5, -270, -94, -265, -793, -183, -761, -516, -608, -218, 57, -889, -912, 508, 93, -90, 34, 530, 201, 999, -37, -186, -62, -980, 239, 902, 983, -287, -634, 524, -772, 470, -961, 32, 162, 315, -411, 400, -235, -283, -787, -703, 869, 792, 543, -274, 239, 733, -439, 306, 349, 579, -200, -201, -824, 384, -246, 133, -508, 770, -102, 957, -825, 740, 748, -376, 183, -426, 46, 668, -886, -43, -174, 672, -419, 390, 927, 1000,\
+318, 886, 47, 908, -540, -825, -5, 314, -999, 354, -603, 966, -633, -689, 985, 534, -290, 167, -652, -797, -612, -79, 488, 622, -464, -950, 595, 897, 704, -238, -395, 125, 831, -180, 226, -379, 310, 564, 56, -978, 895, -61, 686, -251, 434, -417, 161, -512, 752, 528, -589, -425, 66, -925, -157, 1000, 96, 256, -239, -784, -882, -464, -909, 663, -177, -678, -441, 669, -564, -201, -121, -743, 187, -107, -768, -682, 355, 161, 411, 984, -954, 166, -842, -755, 267, -709, 372, -699, -272, -850,\
+403, -839, 949, 622, -62, 51, 917, 70, 528, -558, -632, 832, 276, 61, -445, -195, 960, 846, -474, 764, 879, -411, 948, -62, -592, -123, -96, -551, -555, -724, 849, 250, -808, -732, 797, -839, -554, 306, -919, 888, 484, -728, 152, -122, -287, 16, -345, -396, -268, -963, -500, 433, 343, 418, -480, 828, 594, 821, -9, 933, -230, 707, -847, -610, -748, -234, 688, 935, 713, 865, -743, 293, -143, -20, 928, -906, -762, 528, 722, 412, -70, 622, -245, 539, -686, 730, -866, -705, 28, -916, -623,\
+-768, -614, -915, -123, -183, 680, -223, 515, -37, -235, -5, 260, 347, -239, -322, -861, -848, -936, 945, 721, -580, -639, 780, -153, -26, 685, 177, 587, 307, -915, 435, 658, 539, -229, -719, -171, -858, 162, 734, -539, -437, 246, 639, 765, -477, -342, -209, -284, -779, -414, -452, 914, 338, -83, 759, 567, 266, -485, 14, 225, 347, -432, -242, 997, -365, -764, 119, -641, -416, -388, -436, -388, -54, -649, -571, -920, -477, 714, -363, 836, 369, 702, 869, 503, -287, -679, 46, -666, -202,\
+-602, 71, -259, 967, 601, -571, -830, -993, -271, 281, -494, 482, -180, 572, 587, -651, -566, -448, -228, 511, -924, 832, -52, -712, 402, -644, -533, -865, 269, 965, 56, 675, 179, -338, -272, 614, 602, -283, 303, -70, 909, -942, 117, 839, 468, 813, -765, 884, -697, -813, 352, 374, -705, -295, 633, 211, -754, 597, -941, -142, -393, -469, -653, 688, 996, 911, 214, 431, 453, -141, 874, -81, -258, -735, -3, -110, -338, -929, -182, -306, -104, -840, -588, -759, -157, -801, 848, -698, 627, 914,\
+-33, -353, 425, 150, -798, 553, 934, -778, -196, -132, 808, 745, -894, 144, 213, 662, 273, -79, 454, -60, -467, 48, -15, -807, 69, -930, 749, 559, -867, -103, 258, -677, 750, -303, 846, -227, -936, 744, -770, 770, -434, 594, -477, 589, -612, 535, 357, -623, 683, 369, 905, 980, -410, -663, 762, -888, -563, -845, 843, 353, -491, 996, -255, -336, -132, 695, -823, 289, -143, 365, 916, 877, 245, -530, -848, -804, -118, -108, 847, 620, -355, 499, 881, 92, -640, 542, 38, 626, -260, -34, -378,\
+598, 890, 305, -118, 711, -385, 600, -570, 27, -129, -893, 354, 459, 374, 816, 470, 356, 661, 877, 735, -286, -780, 620, 943, -169, -888, 978, 441, -667, -399, 662, 249, 137, 598, -863, -453, 722, -815, -251, -995, -294, -707, 901, 763, 977, 137, 431, -994, 905, 593, 694, 444, -626, -816, 252, 282, 616, 841, 360, -932, 817, -908, 50, 394, -120, -786, -338, 499, -982, -95, -454, 838, -312, 320, -127, -653, 53, 16, 988, -968, -151, -369, -836, 293, -271, 483, 18, 724, -204, -965, 245, 310,\
+987, 552, -835, -912, -861, 254, 560, 124, 145, 798, 178, 476, 138, -311, 151, -907, -886, -592, 728, -43, -489, 873, -422, -439, -489, 375, -703, -459, 338, 418, -25, 332, -454, 730, -604, -800, 37, -172, -197, -568, -563, -332, 228, -182, 994, -123, 444, -567, 98, 78, 0, -504, -150, 88, -936, 199, -651, -776, 192, 46, 526, -727, -991, 534, -659, -738, 256, -894, 965, -76, 816, 435, -418, 800, 838, 67, -733, 570, 112, -514, -416\r\
+";
+                thread_local! {
+                    static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
+                        build_test_term(&LINE, 5, 50);
+                }
+                TEST_TERM_AND_POINT.with(|(term, point)| {
+                    assert_eq!(
+                        find_from_grid_point_bench(term, *point)
+                            .map(|(path, ..)| path)
+                            .unwrap_or_default(),
+                        "392",
+                        "Hyperlink should have been found"
+                    );
+                });
+            }
+
+            #[perf]
+            // https://github.com/zed-industries/zed/issues/44510
+            pub fn issue_44510_hyperlink_benchmark() {
+                const LINE: &str = "..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+...............................................E.\r\
+";
+                thread_local! {
+                    static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
+                        build_test_term(&LINE, 5, 50);
+                }
+                TEST_TERM_AND_POINT.with(|(term, point)| {
+                    assert_eq!(
+                        find_from_grid_point_bench(term, *point)
+                            .map(|(path, ..)| path)
+                            .unwrap_or_default(),
+                        LINE.trim_end_matches(['.', '\r', '\n']),
                         "Hyperlink should have been found"
                     );
                 });

crates/terminal_view/src/terminal_element.rs πŸ”—

@@ -632,7 +632,7 @@ impl TerminalElement {
     ) -> impl Fn(&E, &mut Window, &mut App) {
         move |event, window, cx| {
             if steal_focus {
-                window.focus(&focus_handle);
+                window.focus(&focus_handle, cx);
             } else if !focus_handle.is_focused(window) {
                 return;
             }
@@ -661,7 +661,7 @@ impl TerminalElement {
             let terminal_view = terminal_view.clone();
 
             move |e, window, cx| {
-                window.focus(&focus);
+                window.focus(&focus, cx);
 
                 let scroll_top = terminal_view.read(cx).scroll_top;
                 terminal.update(cx, |terminal, cx| {

crates/terminal_view/src/terminal_panel.rs πŸ”—

@@ -351,7 +351,7 @@ impl TerminalPanel {
                 } else if let Some(focus_on_pane) =
                     focus_on_pane.as_ref().or_else(|| self.center.panes().pop())
                 {
-                    focus_on_pane.focus_handle(cx).focus(window);
+                    focus_on_pane.focus_handle(cx).focus(window, cx);
                 }
             }
             pane::Event::ZoomIn => {
@@ -397,7 +397,7 @@ impl TerminalPanel {
                                     .center
                                     .split(&pane, &new_pane, direction, cx)
                                     .log_err();
-                                window.focus(&new_pane.focus_handle(cx));
+                                window.focus(&new_pane.focus_handle(cx), cx);
                             })
                             .ok();
                     })
@@ -419,7 +419,7 @@ impl TerminalPanel {
                         pane.add_item(item, true, true, None, window, cx);
                     });
                     self.center.split(&pane, &new_pane, direction, cx).log_err();
-                    window.focus(&new_pane.focus_handle(cx));
+                    window.focus(&new_pane.focus_handle(cx), cx);
                 }
             }
             pane::Event::Focus => {
@@ -790,8 +790,7 @@ impl TerminalPanel {
                 }
 
                 pane.update(cx, |pane, cx| {
-                    let focus = pane.has_focus(window, cx)
-                        || matches!(reveal_strategy, RevealStrategy::Always);
+                    let focus = matches!(reveal_strategy, RevealStrategy::Always);
                     pane.add_item(terminal_view, true, focus, None, window, cx);
                 });
 
@@ -853,8 +852,7 @@ impl TerminalPanel {
                         }
 
                         pane.update(cx, |pane, cx| {
-                            let focus = pane.has_focus(window, cx)
-                                || matches!(reveal_strategy, RevealStrategy::Always);
+                            let focus = matches!(reveal_strategy, RevealStrategy::Always);
                             pane.add_item(terminal_view, true, focus, None, window, cx);
                         });
 
@@ -998,7 +996,7 @@ impl TerminalPanel {
                 RevealStrategy::NoFocus => match reveal_target {
                     RevealTarget::Center => {
                         task_workspace.update_in(cx, |workspace, window, cx| {
-                            workspace.active_pane().focus_handle(cx).focus(window);
+                            workspace.active_pane().focus_handle(cx).focus(window, cx);
                         })?;
                     }
                     RevealTarget::Dock => {
@@ -1053,7 +1051,7 @@ impl TerminalPanel {
             .center
             .find_pane_in_direction(&self.active_pane, direction, cx)
         {
-            window.focus(&pane.focus_handle(cx));
+            window.focus(&pane.focus_handle(cx), cx);
         } else {
             self.workspace
                 .update(cx, |workspace, cx| {
@@ -1171,64 +1169,67 @@ pub fn new_terminal_pane(
                         let source = tab.pane.clone();
                         let item_id_to_move = item.item_id();
 
-                        let Ok(new_split_pane) = pane
-                            .drag_split_direction()
-                            .map(|split_direction| {
-                                drop_closure_terminal_panel.update(cx, |terminal_panel, cx| {
-                                    let is_zoomed = if terminal_panel.active_pane == this_pane {
-                                        pane.is_zoomed()
-                                    } else {
-                                        terminal_panel.active_pane.read(cx).is_zoomed()
-                                    };
-                                    let new_pane = new_terminal_pane(
-                                        workspace.clone(),
-                                        project.clone(),
-                                        is_zoomed,
-                                        window,
-                                        cx,
-                                    );
-                                    terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
-                                    terminal_panel.center.split(
-                                        &this_pane,
-                                        &new_pane,
-                                        split_direction,
-                                        cx,
-                                    )?;
-                                    anyhow::Ok(new_pane)
-                                })
-                            })
-                            .transpose()
-                        else {
-                            return ControlFlow::Break(());
+                        // If no split direction, let the regular pane drop handler take care of it
+                        let Some(split_direction) = pane.drag_split_direction() else {
+                            return ControlFlow::Continue(());
                         };
 
-                        match new_split_pane.transpose() {
-                            // Source pane may be the one currently updated, so defer the move.
-                            Ok(Some(new_pane)) => cx
-                                .spawn_in(window, async move |_, cx| {
-                                    cx.update(|window, cx| {
-                                        move_item(
-                                            &source,
+                        // Gather data synchronously before deferring
+                        let is_zoomed = drop_closure_terminal_panel
+                            .upgrade()
+                            .map(|terminal_panel| {
+                                let terminal_panel = terminal_panel.read(cx);
+                                if terminal_panel.active_pane == this_pane {
+                                    pane.is_zoomed()
+                                } else {
+                                    terminal_panel.active_pane.read(cx).is_zoomed()
+                                }
+                            })
+                            .unwrap_or(false);
+
+                        let workspace = workspace.clone();
+                        let terminal_panel = drop_closure_terminal_panel.clone();
+
+                        // Defer the split operation to avoid re-entrancy panic.
+                        // The pane may be the one currently being updated, so we cannot
+                        // call mark_positions (via split) synchronously.
+                        cx.spawn_in(window, async move |_, cx| {
+                            cx.update(|window, cx| {
+                                let Ok(new_pane) =
+                                    terminal_panel.update(cx, |terminal_panel, cx| {
+                                        let new_pane = new_terminal_pane(
+                                            workspace, project, is_zoomed, window, cx,
+                                        );
+                                        terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
+                                        terminal_panel.center.split(
+                                            &this_pane,
                                             &new_pane,
-                                            item_id_to_move,
-                                            new_pane.read(cx).active_item_index(),
-                                            true,
-                                            window,
+                                            split_direction,
                                             cx,
-                                        );
+                                        )?;
+                                        anyhow::Ok(new_pane)
                                     })
-                                    .ok();
-                                })
-                                .detach(),
-                            // If we drop into existing pane or current pane,
-                            // regular pane drop handler will take care of it,
-                            // using the right tab index for the operation.
-                            Ok(None) => return ControlFlow::Continue(()),
-                            err @ Err(_) => {
-                                err.log_err();
-                                return ControlFlow::Break(());
-                            }
-                        };
+                                else {
+                                    return;
+                                };
+
+                                let Some(new_pane) = new_pane.log_err() else {
+                                    return;
+                                };
+
+                                move_item(
+                                    &source,
+                                    &new_pane,
+                                    item_id_to_move,
+                                    new_pane.read(cx).active_item_index(),
+                                    true,
+                                    window,
+                                    cx,
+                                );
+                            })
+                            .ok();
+                        })
+                        .detach();
                     } else if let Some(project_path) = item.project_path(cx)
                         && let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
                     {
@@ -1297,7 +1298,7 @@ fn add_paths_to_terminal(
         .active_item()
         .and_then(|item| item.downcast::<TerminalView>())
     {
-        window.focus(&terminal_view.focus_handle(cx));
+        window.focus(&terminal_view.focus_handle(cx), cx);
         let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join("");
         new_text.push(' ');
         terminal_view.update(cx, |terminal_view, cx| {
@@ -1451,7 +1452,7 @@ impl Render for TerminalPanel {
                             .position(|pane| **pane == terminal_panel.active_pane)
                         {
                             let next_ix = (ix + 1) % panes.len();
-                            window.focus(&panes[next_ix].focus_handle(cx));
+                            window.focus(&panes[next_ix].focus_handle(cx), cx);
                         }
                     }),
                 )
@@ -1463,7 +1464,7 @@ impl Render for TerminalPanel {
                             .position(|pane| **pane == terminal_panel.active_pane)
                         {
                             let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
-                            window.focus(&panes[prev_ix].focus_handle(cx));
+                            window.focus(&panes[prev_ix].focus_handle(cx), cx);
                         }
                     },
                 ))
@@ -1471,7 +1472,7 @@ impl Render for TerminalPanel {
                     cx.listener(|terminal_panel, action: &ActivatePane, window, cx| {
                         let panes = terminal_panel.center.panes();
                         if let Some(&pane) = panes.get(action.0) {
-                            window.focus(&pane.read(cx).focus_handle(cx));
+                            window.focus(&pane.read(cx).focus_handle(cx), cx);
                         } else {
                             let future =
                                 terminal_panel.new_pane_with_cloned_active_terminal(window, cx);
@@ -1490,7 +1491,7 @@ impl Render for TerminalPanel {
                                                 )
                                                 .log_err();
                                             let new_pane = new_pane.read(cx);
-                                            window.focus(&new_pane.focus_handle(cx));
+                                            window.focus(&new_pane.focus_handle(cx), cx);
                                         },
                                     );
                                 }

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

@@ -8,8 +8,8 @@ mod terminal_slash_command;
 use assistant_slash_command::SlashCommandRegistry;
 use editor::{EditorSettings, actions::SelectAll, blink_manager::BlinkManager};
 use gpui::{
-    Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
-    KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
+    Action, AnyElement, App, ClipboardEntry, DismissEvent, Entity, EventEmitter, FocusHandle,
+    Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
     ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
 };
 use persistence::TERMINAL_DB;
@@ -409,7 +409,7 @@ impl TerminalView {
                 )
         });
 
-        window.focus(&context_menu.focus_handle(cx));
+        window.focus(&context_menu.focus_handle(cx), cx);
         let subscription = cx.subscribe_in(
             &context_menu,
             window,
@@ -687,12 +687,32 @@ impl TerminalView {
 
     ///Attempt to paste the clipboard into the terminal
     fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
-        if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) {
+        let Some(clipboard) = cx.read_from_clipboard() else {
+            return;
+        };
+
+        if clipboard.entries().iter().any(|entry| match entry {
+            ClipboardEntry::Image(image) => !image.bytes.is_empty(),
+            _ => false,
+        }) {
+            self.forward_ctrl_v(cx);
+            return;
+        }
+
+        if let Some(text) = clipboard.text() {
             self.terminal
-                .update(cx, |terminal, _cx| terminal.paste(&clipboard_string));
+                .update(cx, |terminal, _cx| terminal.paste(&text));
         }
     }
 
+    /// Emits a raw Ctrl+V so TUI agents can read the OS clipboard directly
+    /// and attach images using their native workflows.
+    fn forward_ctrl_v(&self, cx: &mut Context<Self>) {
+        self.terminal.update(cx, |term, _| {
+            term.input(vec![0x16]);
+        });
+    }
+
     fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context<Self>) {
         self.clear_bell(cx);
         self.terminal.update(cx, |term, _| {

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

@@ -605,7 +605,7 @@ impl TitleBar {
                 })
                 .on_click(move |_, window, cx| {
                     let _ = workspace.update(cx, |this, cx| {
-                        window.focus(&this.active_pane().focus_handle(cx));
+                        window.focus(&this.active_pane().focus_handle(cx), cx);
                         window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
                     });
                 })

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

@@ -225,7 +225,7 @@ impl AddToolchainState {
                                 );
                             });
                             *input_state = Self::wait_for_path(rx, window, cx);
-                            this.focus_handle(cx).focus(window);
+                            this.focus_handle(cx).focus(window, cx);
                         }
                     });
                     return Err(anyhow::anyhow!("Failed to resolve toolchain"));
@@ -260,7 +260,7 @@ impl AddToolchainState {
                         toolchain,
                         scope_picker,
                     };
-                    this.focus_handle(cx).focus(window);
+                    this.focus_handle(cx).focus(window, cx);
                 });
 
                 Result::<_, anyhow::Error>::Ok(())
@@ -333,7 +333,7 @@ impl AddToolchainState {
         });
         _ = self.weak.update(cx, |this, cx| {
             this.state = State::Search((this.create_search_state)(window, cx));
-            this.focus_handle(cx).focus(window);
+            this.focus_handle(cx).focus(window, cx);
             cx.notify();
         });
     }
@@ -383,7 +383,7 @@ impl Render for AddToolchainState {
                     &weak,
                     |this: &mut ToolchainSelector, _: &menu::Cancel, window, cx| {
                         this.state = State::Search((this.create_search_state)(window, cx));
-                        this.state.focus_handle(cx).focus(window);
+                        this.state.focus_handle(cx).focus(window, cx);
                         cx.notify();
                     },
                 ))
@@ -703,7 +703,7 @@ impl ToolchainSelector {
                 window,
                 cx,
             ));
-            self.state.focus_handle(cx).focus(window);
+            self.state.focus_handle(cx).focus(window, cx);
             cx.notify();
         }
     }

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

@@ -17,7 +17,6 @@ mod icon;
 mod image;
 mod indent_guides;
 mod indicator;
-mod inline_code;
 mod keybinding;
 mod keybinding_hint;
 mod label;
@@ -64,7 +63,6 @@ pub use icon::*;
 pub use image::*;
 pub use indent_guides::*;
 pub use indicator::*;
-pub use inline_code::*;
 pub use keybinding::*;
 pub use keybinding_hint::*;
 pub use label::*;

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

@@ -121,7 +121,7 @@ impl RenderOnce for Callout {
             Severity::Info => (
                 IconName::Info,
                 Color::Muted,
-                cx.theme().colors().panel_background.opacity(0.),
+                cx.theme().status().info_background.opacity(0.1),
             ),
             Severity::Success => (
                 IconName::Check,

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

@@ -562,7 +562,7 @@ impl ContextMenu {
             action: Some(action.boxed_clone()),
             handler: Rc::new(move |context, window, cx| {
                 if let Some(context) = &context {
-                    window.focus(context);
+                    window.focus(context, cx);
                 }
                 window.dispatch_action(action.boxed_clone(), cx);
             }),
@@ -594,7 +594,7 @@ impl ContextMenu {
             action: Some(action.boxed_clone()),
             handler: Rc::new(move |context, window, cx| {
                 if let Some(context) = &context {
-                    window.focus(context);
+                    window.focus(context, cx);
                 }
                 window.dispatch_action(action.boxed_clone(), cx);
             }),

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

@@ -1,64 +0,0 @@
-use crate::prelude::*;
-use gpui::{AnyElement, IntoElement, ParentElement, Styled};
-
-/// InlineCode mimics the way inline code is rendered when wrapped in backticks in Markdown.
-///
-/// # Usage Example
-///
-/// ```
-/// use ui::InlineCode;
-///
-/// let InlineCode = InlineCode::new("<div>hey</div>");
-/// ```
-#[derive(IntoElement, RegisterComponent)]
-pub struct InlineCode {
-    label: SharedString,
-    label_size: LabelSize,
-}
-
-impl InlineCode {
-    pub fn new(label: impl Into<SharedString>) -> Self {
-        Self {
-            label: label.into(),
-            label_size: LabelSize::Default,
-        }
-    }
-
-    /// Sets the size of the label.
-    pub fn label_size(mut self, size: LabelSize) -> Self {
-        self.label_size = size;
-        self
-    }
-}
-
-impl RenderOnce for InlineCode {
-    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
-        h_flex()
-            .min_w_0()
-            .px_0p5()
-            .overflow_hidden()
-            .bg(cx.theme().colors().text.opacity(0.05))
-            .child(Label::new(self.label).size(self.label_size).buffer_font(cx))
-    }
-}
-
-impl Component for InlineCode {
-    fn scope() -> ComponentScope {
-        ComponentScope::DataDisplay
-    }
-
-    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
-        Some(
-            v_flex()
-                .gap_6()
-                .child(
-                    example_group(vec![single_example(
-                        "Simple",
-                        InlineCode::new("zed.dev").into_any_element(),
-                    )])
-                    .vertical(),
-                )
-                .into_any_element(),
-        )
-    }
-}

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

@@ -75,7 +75,7 @@ impl RenderOnce for Navigable {
                         })
                         .unwrap_or(0);
                     if let Some(entry) = children.get(target) {
-                        entry.focus_handle.focus(window);
+                        entry.focus_handle.focus(window, cx);
                         if let Some(anchor) = &entry.scroll_anchor {
                             anchor.scroll_to(window, cx);
                         }
@@ -89,7 +89,7 @@ impl RenderOnce for Navigable {
                         .and_then(|index| index.checked_sub(1))
                         .or(children.len().checked_sub(1));
                     if let Some(entry) = target.and_then(|target| children.get(target)) {
-                        entry.focus_handle.focus(window);
+                        entry.focus_handle.focus(window, cx);
                         if let Some(anchor) = &entry.scroll_anchor {
                             anchor.scroll_to(window, cx);
                         }

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

@@ -281,13 +281,25 @@ fn show_menu<M: ManagedView>(
             if modal.focus_handle(cx).contains_focused(window, cx)
                 && let Some(previous_focus_handle) = previous_focus_handle.as_ref()
             {
-                window.focus(previous_focus_handle);
+                window.focus(previous_focus_handle, cx);
             }
             *menu2.borrow_mut() = None;
             window.refresh();
         })
         .detach();
-    window.focus(&new_menu.focus_handle(cx));
+
+    // Since menus are rendered in a deferred fashion, their focus handles are
+    // not linked in the dispatch tree until after the deferred draw callback
+    // runs. We need to wait for that to happen before focusing it, so that
+    // calling `contains_focused` on the parent's focus handle returns `true`
+    // when the menu is focused. This prevents the pane's tab bar buttons from
+    // flickering when opening popover menus.
+    let focus_handle = new_menu.focus_handle(cx);
+    window.on_next_frame(move |window, _cx| {
+        window.on_next_frame(move |window, cx| {
+            window.focus(&focus_handle, cx);
+        });
+    });
     *menu.borrow_mut() = Some(new_menu);
     window.refresh();
 

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

@@ -253,13 +253,25 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
                                     && let Some(previous_focus_handle) =
                                         previous_focus_handle.as_ref()
                                 {
-                                    window.focus(previous_focus_handle);
+                                    window.focus(previous_focus_handle, cx);
                                 }
                                 *menu2.borrow_mut() = None;
                                 window.refresh();
                             })
                             .detach();
-                        window.focus(&new_menu.focus_handle(cx));
+
+                        // Since menus are rendered in a deferred fashion, their focus handles are
+                        // not linked in the dispatch tree until after the deferred draw callback
+                        // runs. We need to wait for that to happen before focusing it, so that
+                        // calling `contains_focused` on the parent's focus handle returns `true`
+                        // when the menu is focused. This prevents the pane's tab bar buttons from
+                        // flickering when opening menus.
+                        let focus_handle = new_menu.focus_handle(cx);
+                        window.on_next_frame(move |window, _cx| {
+                            window.on_next_frame(move |window, cx| {
+                                window.focus(&focus_handle, cx);
+                            });
+                        });
                         *menu.borrow_mut() = Some(new_menu);
                         *position.borrow_mut() = if let Some(child_bounds) = child_bounds {
                             if let Some(attach) = attach {

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

@@ -476,7 +476,7 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
                                                         if let Some(previous) =
                                                             previous_focus_handle.as_ref()
                                                         {
-                                                            window.focus(previous);
+                                                            window.focus(previous, cx);
                                                         }
                                                         on_change(&new_value, window, cx);
                                                     };
@@ -485,7 +485,7 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
                                             })
                                             .detach();
 
-                                            window.focus(&editor.focus_handle(cx));
+                                            window.focus(&editor.focus_handle(cx), cx);
 
                                             editor
                                         }

crates/util/src/redact.rs πŸ”—

@@ -1,3 +1,9 @@
+use std::sync::LazyLock;
+
+static REDACT_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
+    regex::Regex::new(r#"([A-Z_][A-Z0-9_]*)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)"#).unwrap()
+});
+
 /// Whether a given environment variable name should have its value redacted
 pub fn should_redact(env_var_name: &str) -> bool {
     const REDACTED_SUFFIXES: &[&str] = &[
@@ -13,3 +19,31 @@ pub fn should_redact(env_var_name: &str) -> bool {
         .iter()
         .any(|suffix| env_var_name.ends_with(suffix))
 }
+
+/// Redact a string which could include a command with environment variables
+pub fn redact_command(command: &str) -> String {
+    REDACT_REGEX
+        .replace_all(command, |caps: &regex::Captures| {
+            let var_name = &caps[1];
+            let value = &caps[2];
+            if should_redact(var_name) {
+                format!(r#"{}="[REDACTED]""#, var_name)
+            } else {
+                format!("{}={}", var_name, value)
+            }
+        })
+        .to_string()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_redact_string_with_multiple_env_vars() {
+        let input = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="sk-ant-api03-WOOOO" COMMAND_MODE="unix2003" GEMINI_API_KEY="AIGEMINIFACE" HOME="/Users/foo""#;
+        let result = redact_command(input);
+        let expected = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="[REDACTED]" COMMAND_MODE="unix2003" GEMINI_API_KEY="[REDACTED]" HOME="/Users/foo""#;
+        assert_eq!(result, expected);
+    }
+}

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

@@ -330,10 +330,12 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
                 let Some(range) = range.buffer_range(vim, editor, window, cx).ok() else {
                     return;
                 };
-                let Some((line_ending, text, whole_buffer)) = editor.buffer().update(cx, |multi, cx| {
+                let Some((line_ending, encoding, has_bom, text, whole_buffer)) = editor.buffer().update(cx, |multi, cx| {
                     Some(multi.as_singleton()?.update(cx, |buffer, _| {
                         (
                             buffer.line_ending(),
+                            buffer.encoding(),
+                            buffer.has_bom(),
                             buffer.as_rope().slice_rows(range.start.0..range.end.0 + 1),
                             range.start.0 == 0 && range.end.0 + 1 >= buffer.row_count(),
                         )
@@ -429,7 +431,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
                                     return;
                                 };
                                 worktree
-                                    .write_file(path.into_arc(), text.clone(), line_ending, cx)
+                                    .write_file(path.into_arc(), text.clone(), line_ending, encoding, has_bom, cx)
                                     .detach_and_prompt_err("Failed to write lines", window, cx, |_, _, _| None);
                             });
                         })

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

@@ -1,5 +1,5 @@
 use editor::{
-    Anchor, Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, RowExt, ToOffset, ToPoint,
+    Anchor, Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, RowExt, ToOffset,
     display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
     movement::{
         self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point,
@@ -2262,7 +2262,6 @@ fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -
             .offset_to_point(excerpt.map_offset_from_buffer(BufferOffset(offset)));
         return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
     }
-    let mut last_position = None;
     for (excerpt, buffer, range) in map.buffer_snapshot().excerpts() {
         let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer)
             ..language::ToOffset::to_offset(&range.context.end, buffer);
@@ -2273,14 +2272,9 @@ fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -
         } else if offset <= excerpt_range.start {
             let anchor = Anchor::in_buffer(excerpt, range.context.start);
             return anchor.to_display_point(map);
-        } else {
-            last_position = Some(Anchor::in_buffer(excerpt, range.context.end));
         }
     }
 
-    let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot());
-    last_point.column = point.column;
-
     map.clip_point(
         map.point_to_display_point(
             map.buffer_snapshot().clip_point(point, Bias::Left),

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

@@ -2807,9 +2807,8 @@ mod test {
 
         for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
             cx.set_state(initial_state, Mode::Normal);
-
+            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
             cx.simulate_keystrokes(keystrokes);
-
             cx.assert_state(expected_state, *expected_mode);
         }
 
@@ -2830,9 +2829,8 @@ mod test {
 
         for (keystrokes, initial_state, mode) in INVALID_CASES {
             cx.set_state(initial_state, Mode::Normal);
-
+            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
             cx.simulate_keystrokes(keystrokes);
-
             cx.assert_state(initial_state, *mode);
         }
     }
@@ -3185,9 +3183,8 @@ mod test {
 
         for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES {
             cx.set_state(initial_state, Mode::Normal);
-
+            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
             cx.simulate_keystrokes(keystrokes);
-
             cx.assert_state(expected_state, *expected_mode);
         }
 
@@ -3208,9 +3205,8 @@ mod test {
 
         for (keystrokes, initial_state, mode) in INVALID_CASES {
             cx.set_state(initial_state, Mode::Normal);
-
+            cx.buffer(|buffer, _| buffer.parsing_idle()).await;
             cx.simulate_keystrokes(keystrokes);
-
             cx.assert_state(initial_state, *mode);
         }
     }

crates/which_key/Cargo.toml πŸ”—

@@ -0,0 +1,23 @@
+[package]
+name = "which_key"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/which_key.rs"
+doctest = false
+
+[dependencies]
+command_palette.workspace = true
+gpui.workspace = true
+serde.workspace = true
+settings.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true

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

@@ -0,0 +1,98 @@
+//! Which-key support for Zed.
+
+mod which_key_modal;
+mod which_key_settings;
+
+use gpui::{App, Keystroke};
+use settings::Settings;
+use std::{sync::LazyLock, time::Duration};
+use util::ResultExt;
+use which_key_modal::WhichKeyModal;
+use which_key_settings::WhichKeySettings;
+use workspace::Workspace;
+
+pub fn init(cx: &mut App) {
+    WhichKeySettings::register(cx);
+
+    cx.observe_new(|_: &mut Workspace, window, cx| {
+        let Some(window) = window else {
+            return;
+        };
+        let mut timer = None;
+        cx.observe_pending_input(window, move |workspace, window, cx| {
+            if window.pending_input_keystrokes().is_none() {
+                if let Some(modal) = workspace.active_modal::<WhichKeyModal>(cx) {
+                    modal.update(cx, |modal, cx| modal.dismiss(cx));
+                };
+                timer.take();
+                return;
+            }
+
+            let which_key_settings = WhichKeySettings::get_global(cx);
+            if !which_key_settings.enabled {
+                return;
+            }
+
+            let delay_ms = which_key_settings.delay_ms;
+
+            timer.replace(cx.spawn_in(window, async move |workspace_handle, cx| {
+                cx.background_executor()
+                    .timer(Duration::from_millis(delay_ms))
+                    .await;
+                workspace_handle
+                    .update_in(cx, |workspace, window, cx| {
+                        if workspace.active_modal::<WhichKeyModal>(cx).is_some() {
+                            return;
+                        };
+
+                        workspace.toggle_modal(window, cx, |window, cx| {
+                            WhichKeyModal::new(workspace_handle.clone(), window, cx)
+                        });
+                    })
+                    .log_err();
+            }));
+        })
+        .detach();
+    })
+    .detach();
+}
+
+// Hard-coded list of keystrokes to filter out from which-key display
+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",
+        "ctrl-w ctrl-h",
+        "ctrl-w ctrl-j",
+        "ctrl-w ctrl-k",
+        "ctrl-w ctrl-l",
+        "ctrl-w ctrl-n",
+        "ctrl-w ctrl-o",
+        "ctrl-w ctrl-p",
+        "ctrl-w ctrl-q",
+        "ctrl-w ctrl-s",
+        "ctrl-w ctrl-v",
+        "ctrl-w ctrl-w",
+        "ctrl-w ctrl-]",
+        "ctrl-w ctrl-shift-w",
+        "ctrl-w ctrl-g t",
+        "ctrl-w ctrl-g shift-t",
+    ]
+    .iter()
+    .filter_map(|s| {
+        let keystrokes: Result<Vec<_>, _> = s
+            .split(' ')
+            .map(|keystroke_str| Keystroke::parse(keystroke_str))
+            .collect();
+        keystrokes.ok()
+    })
+    .collect()
+});

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

@@ -0,0 +1,308 @@
+//! Modal implementation for the which-key display.
+
+use gpui::prelude::FluentBuilder;
+use gpui::{
+    App, Context, DismissEvent, EventEmitter, FocusHandle, Focusable, FontWeight, Keystroke,
+    ScrollHandle, Subscription, WeakEntity, Window,
+};
+use settings::Settings;
+use std::collections::HashMap;
+use theme::ThemeSettings;
+use ui::{
+    Divider, DividerColor, DynamicSpacing, LabelSize, WithScrollbar, prelude::*,
+    text_for_keystrokes,
+};
+use workspace::{ModalView, Workspace};
+
+use crate::FILTERED_KEYSTROKES;
+
+pub struct WhichKeyModal {
+    _workspace: WeakEntity<Workspace>,
+    focus_handle: FocusHandle,
+    scroll_handle: ScrollHandle,
+    bindings: Vec<(SharedString, SharedString)>,
+    pending_keys: SharedString,
+    _pending_input_subscription: Subscription,
+    _focus_out_subscription: Subscription,
+}
+
+impl WhichKeyModal {
+    pub fn new(
+        workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        // Keep focus where it currently is
+        let focus_handle = window.focused(cx).unwrap_or(cx.focus_handle());
+
+        let handle = cx.weak_entity();
+        let mut this = Self {
+            _workspace: workspace,
+            focus_handle: focus_handle.clone(),
+            scroll_handle: ScrollHandle::new(),
+            bindings: Vec::new(),
+            pending_keys: SharedString::new_static(""),
+            _pending_input_subscription: cx.observe_pending_input(
+                window,
+                |this: &mut Self, window, cx| {
+                    this.update_pending_keys(window, cx);
+                },
+            ),
+            _focus_out_subscription: window.on_focus_out(&focus_handle, cx, move |_, _, cx| {
+                handle.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
+            }),
+        };
+        this.update_pending_keys(window, cx);
+        this
+    }
+
+    pub fn dismiss(&self, cx: &mut Context<Self>) {
+        cx.emit(DismissEvent)
+    }
+
+    fn update_pending_keys(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(pending_keys) = window.pending_input_keystrokes() else {
+            cx.emit(DismissEvent);
+            return;
+        };
+        let bindings = window.possible_bindings_for_input(pending_keys);
+
+        let mut binding_data = bindings
+            .iter()
+            .map(|binding| {
+                // Map to keystrokes
+                (
+                    binding
+                        .keystrokes()
+                        .iter()
+                        .map(|k| k.inner().to_owned())
+                        .collect::<Vec<_>>(),
+                    binding.action(),
+                )
+            })
+            .filter(|(keystrokes, _action)| {
+                // Check if this binding matches any filtered keystroke pattern
+                !FILTERED_KEYSTROKES.iter().any(|filtered| {
+                    keystrokes.len() >= filtered.len()
+                        && keystrokes[..filtered.len()] == filtered[..]
+                })
+            })
+            .map(|(keystrokes, action)| {
+                // Map to remaining keystrokes and action name
+                let remaining_keystrokes = keystrokes[pending_keys.len()..].to_vec();
+                let action_name: SharedString =
+                    command_palette::humanize_action_name(action.name()).into();
+                (remaining_keystrokes, action_name)
+            })
+            .collect();
+
+        binding_data = group_bindings(binding_data);
+
+        // Sort bindings from shortest to longest, with groups last
+        // Using stable sort to preserve relative order of equal elements
+        binding_data.sort_by(|(keystrokes_a, action_a), (keystrokes_b, action_b)| {
+            // Groups (actions starting with "+") should go last
+            let is_group_a = action_a.starts_with('+');
+            let is_group_b = action_b.starts_with('+');
+
+            // First, separate groups from non-groups
+            let group_cmp = is_group_a.cmp(&is_group_b);
+            if group_cmp != std::cmp::Ordering::Equal {
+                return group_cmp;
+            }
+
+            // Then sort by keystroke count
+            let keystroke_cmp = keystrokes_a.len().cmp(&keystrokes_b.len());
+            if keystroke_cmp != std::cmp::Ordering::Equal {
+                return keystroke_cmp;
+            }
+
+            // Finally sort by text length, then lexicographically for full stability
+            let text_a = text_for_keystrokes(keystrokes_a, cx);
+            let text_b = text_for_keystrokes(keystrokes_b, cx);
+            let text_len_cmp = text_a.len().cmp(&text_b.len());
+            if text_len_cmp != std::cmp::Ordering::Equal {
+                return text_len_cmp;
+            }
+            text_a.cmp(&text_b)
+        });
+        binding_data.dedup();
+        self.pending_keys = text_for_keystrokes(&pending_keys, cx).into();
+        self.bindings = binding_data
+            .into_iter()
+            .map(|(keystrokes, action)| (text_for_keystrokes(&keystrokes, cx).into(), action))
+            .collect();
+    }
+}
+
+impl Render for WhichKeyModal {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let has_rows = !self.bindings.is_empty();
+        let viewport_size = window.viewport_size();
+
+        let max_panel_width = px((f32::from(viewport_size.width) * 0.5).min(480.0));
+        let max_content_height = px(f32::from(viewport_size.height) * 0.4);
+
+        // Push above status bar when visible
+        let status_height = self
+            ._workspace
+            .upgrade()
+            .and_then(|workspace| {
+                workspace.read_with(cx, |workspace, cx| {
+                    if workspace.status_bar_visible(cx) {
+                        Some(
+                            DynamicSpacing::Base04.px(cx) * 2.0
+                                + ThemeSettings::get_global(cx).ui_font_size(cx),
+                        )
+                    } else {
+                        None
+                    }
+                })
+            })
+            .unwrap_or(px(0.));
+
+        let margin_bottom = px(16.);
+        let bottom_offset = margin_bottom + status_height;
+
+        // Title section
+        let title_section = {
+            let mut column = v_flex().gap(px(0.)).child(
+                div()
+                    .child(
+                        Label::new(self.pending_keys.clone())
+                            .size(LabelSize::Default)
+                            .weight(FontWeight::MEDIUM)
+                            .color(Color::Accent),
+                    )
+                    .mb(px(2.)),
+            );
+
+            if has_rows {
+                column = column.child(
+                    div()
+                        .child(Divider::horizontal().color(DividerColor::BorderFaded))
+                        .mb(px(2.)),
+                );
+            }
+
+            column
+        };
+
+        let content = h_flex()
+            .items_start()
+            .id("which-key-content")
+            .gap(px(8.))
+            .overflow_y_scroll()
+            .track_scroll(&self.scroll_handle)
+            .h_full()
+            .max_h(max_content_height)
+            .child(
+                // Keystrokes column
+                v_flex()
+                    .gap(px(4.))
+                    .flex_shrink_0()
+                    .children(self.bindings.iter().map(|(keystrokes, _)| {
+                        div()
+                            .child(
+                                Label::new(keystrokes.clone())
+                                    .size(LabelSize::Default)
+                                    .color(Color::Accent),
+                            )
+                            .text_align(gpui::TextAlign::Right)
+                    })),
+            )
+            .child(
+                // Actions column
+                v_flex()
+                    .gap(px(4.))
+                    .flex_1()
+                    .min_w_0()
+                    .children(self.bindings.iter().map(|(_, action_name)| {
+                        let is_group = action_name.starts_with('+');
+                        let label_color = if is_group {
+                            Color::Success
+                        } else {
+                            Color::Default
+                        };
+
+                        div().child(
+                            Label::new(action_name.clone())
+                                .size(LabelSize::Default)
+                                .color(label_color)
+                                .single_line()
+                                .truncate(),
+                        )
+                    })),
+            );
+
+        div()
+            .id("which-key-buffer-panel-scroll")
+            .occlude()
+            .absolute()
+            .bottom(bottom_offset)
+            .right(px(16.))
+            .min_w(px(220.))
+            .max_w(max_panel_width)
+            .elevation_3(cx)
+            .px(px(12.))
+            .child(v_flex().child(title_section).when(has_rows, |el| {
+                el.child(
+                    div()
+                        .max_h(max_content_height)
+                        .child(content)
+                        .vertical_scrollbar_for(&self.scroll_handle, window, cx),
+                )
+            }))
+    }
+}
+
+impl EventEmitter<DismissEvent> for WhichKeyModal {}
+
+impl Focusable for WhichKeyModal {
+    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl ModalView for WhichKeyModal {
+    fn render_bare(&self) -> bool {
+        true
+    }
+}
+
+fn group_bindings(
+    binding_data: Vec<(Vec<Keystroke>, SharedString)>,
+) -> Vec<(Vec<Keystroke>, SharedString)> {
+    let mut groups: HashMap<Option<Keystroke>, Vec<(Vec<Keystroke>, SharedString)>> =
+        HashMap::new();
+
+    // Group bindings by their first keystroke
+    for (remaining_keystrokes, action_name) in binding_data {
+        let first_key = remaining_keystrokes.first().cloned();
+        groups
+            .entry(first_key)
+            .or_default()
+            .push((remaining_keystrokes, action_name));
+    }
+
+    let mut result = Vec::new();
+
+    for (first_key, mut group_bindings) in groups {
+        // Remove duplicates within each group
+        group_bindings.dedup_by_key(|(keystrokes, _)| keystrokes.clone());
+
+        if let Some(first_key) = first_key
+            && group_bindings.len() > 1
+        {
+            // This is a group - create a single entry with just the first keystroke
+            let first_keystroke = vec![first_key];
+            let count = group_bindings.len();
+            result.push((first_keystroke, format!("+{} keybinds", count).into()));
+        } else {
+            // Not a group or empty keystrokes - add all bindings as-is
+            result.append(&mut group_bindings);
+        }
+    }
+
+    result
+}

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

@@ -0,0 +1,18 @@
+use settings::{RegisterSetting, Settings, SettingsContent, WhichKeySettingsContent};
+
+#[derive(Debug, Clone, Copy, RegisterSetting)]
+pub struct WhichKeySettings {
+    pub enabled: bool,
+    pub delay_ms: u64,
+}
+
+impl Settings for WhichKeySettings {
+    fn from_settings(content: &SettingsContent) -> Self {
+        let which_key: &WhichKeySettingsContent = content.which_key.as_ref().unwrap();
+
+        Self {
+            enabled: which_key.enabled.unwrap(),
+            delay_ms: which_key.delay_ms.unwrap(),
+        }
+    }
+}

crates/workspace/src/dock.rs πŸ”—

@@ -1,5 +1,4 @@
 use crate::persistence::model::DockData;
-use crate::utility_pane::utility_slot_for_dock_position;
 use crate::{DraggedDock, Event, ModalLayer, Pane};
 use crate::{Workspace, status_bar::StatusItemView};
 use anyhow::Context as _;
@@ -350,7 +349,7 @@ impl Dock {
             let focus_subscription =
                 cx.on_focus(&focus_handle, window, |dock: &mut Dock, window, cx| {
                     if let Some(active_entry) = dock.active_panel_entry() {
-                        active_entry.panel.panel_focus_handle(cx).focus(window)
+                        active_entry.panel.panel_focus_handle(cx).focus(window, cx)
                     }
                 });
             let zoom_subscription = cx.subscribe(&workspace, |dock, workspace, e: &Event, cx| {
@@ -593,7 +592,7 @@ impl Dock {
                         this.set_panel_zoomed(&panel.to_any(), true, window, cx);
                         if !PanelHandle::panel_focus_handle(panel, cx).contains_focused(window, cx)
                         {
-                            window.focus(&panel.focus_handle(cx));
+                            window.focus(&panel.focus_handle(cx), cx);
                         }
                         workspace
                             .update(cx, |workspace, cx| {
@@ -625,7 +624,7 @@ impl Dock {
                         {
                             this.set_open(true, window, cx);
                             this.activate_panel(ix, window, cx);
-                            window.focus(&panel.read(cx).focus_handle(cx));
+                            window.focus(&panel.read(cx).focus_handle(cx), cx);
                         }
                     }
                     PanelEvent::Close => {
@@ -705,7 +704,7 @@ impl Dock {
         panel: &Entity<T>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) {
+    ) -> bool {
         if let Some(panel_ix) = self
             .panel_entries
             .iter()
@@ -724,15 +723,12 @@ impl Dock {
                 }
             }
 
-            let slot = utility_slot_for_dock_position(self.position);
-            if let Some(workspace) = self.workspace.upgrade() {
-                workspace.update(cx, |workspace, cx| {
-                    workspace.clear_utility_pane_if_provider(slot, Entity::entity_id(panel), cx);
-                });
-            }
-
             self.panel_entries.remove(panel_ix);
             cx.notify();
+
+            true
+        } else {
+            false
         }
     }
 
@@ -1052,7 +1048,7 @@ impl Render for PanelButtons {
                                             name = name,
                                             toggle_state = !is_open
                                         );
-                                        window.focus(&focus_handle);
+                                        window.focus(&focus_handle, cx);
                                         window.dispatch_action(action.boxed_clone(), cx)
                                     }
                                 })

crates/workspace/src/item.rs πŸ”—

@@ -76,7 +76,13 @@ impl Settings for ItemSettings {
     fn from_settings(content: &settings::SettingsContent) -> Self {
         let tabs = content.tabs.as_ref().unwrap();
         Self {
-            git_status: tabs.git_status.unwrap(),
+            git_status: tabs.git_status.unwrap()
+                && content
+                    .git
+                    .unwrap()
+                    .enabled
+                    .unwrap()
+                    .is_git_status_enabled(),
             close_position: tabs.close_position.unwrap(),
             activate_on_close: tabs.activate_on_close.unwrap(),
             file_icons: tabs.file_icons.unwrap(),
@@ -886,8 +892,12 @@ impl<T: Item> ItemHandle for Entity<T> {
                         // Only trigger autosave if focus has truly left the item.
                         // If focus is still within the item's hierarchy (e.g., moved to a context menu),
                         // don't trigger autosave to avoid unwanted formatting and cursor jumps.
+                        // Also skip autosave if focus moved to a modal (e.g., command palette),
+                        // since the user is still interacting with the workspace.
                         let focus_handle = item.item_focus_handle(cx);
-                        if !focus_handle.contains_focused(window, cx) {
+                        if !focus_handle.contains_focused(window, cx)
+                            && !workspace.has_active_modal(window, cx)
+                        {
                             Pane::autosave_item(&item, workspace.project.clone(), window, cx)
                                 .detach_and_log_err(cx);
                         }
@@ -1042,7 +1052,7 @@ impl<T: Item> ItemHandle for Entity<T> {
 
     fn relay_action(&self, action: Box<dyn Action>, window: &mut Window, cx: &mut App) {
         self.update(cx, |this, cx| {
-            this.focus_handle(cx).focus(window);
+            this.focus_handle(cx).focus(window, cx);
             window.dispatch_action(action, cx);
         })
     }

crates/workspace/src/modal_layer.rs πŸ”—

@@ -22,12 +22,17 @@ pub trait ModalView: ManagedView {
     fn fade_out_background(&self) -> bool {
         false
     }
+
+    fn render_bare(&self) -> bool {
+        false
+    }
 }
 
 trait ModalViewHandle {
     fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision;
     fn view(&self) -> AnyView;
     fn fade_out_background(&self, cx: &mut App) -> bool;
+    fn render_bare(&self, cx: &mut App) -> bool;
 }
 
 impl<V: ModalView> ModalViewHandle for Entity<V> {
@@ -42,6 +47,10 @@ impl<V: ModalView> ModalViewHandle for Entity<V> {
     fn fade_out_background(&self, cx: &mut App) -> bool {
         self.read(cx).fade_out_background()
     }
+
+    fn render_bare(&self, cx: &mut App) -> bool {
+        self.read(cx).render_bare()
+    }
 }
 
 pub struct ActiveModal {
@@ -116,7 +125,7 @@ impl ModalLayer {
             focus_handle,
         });
         cx.defer_in(window, move |_, window, cx| {
-            window.focus(&new_modal.focus_handle(cx));
+            window.focus(&new_modal.focus_handle(cx), cx);
         });
         cx.notify();
     }
@@ -144,7 +153,7 @@ impl ModalLayer {
             if let Some(previous_focus) = active_modal.previous_focus_handle
                 && active_modal.focus_handle.contains_focused(window, cx)
             {
-                previous_focus.focus(window);
+                previous_focus.focus(window, cx);
             }
             cx.notify();
         }
@@ -167,9 +176,13 @@ impl ModalLayer {
 impl Render for ModalLayer {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let Some(active_modal) = &self.active_modal else {
-            return div();
+            return div().into_any_element();
         };
 
+        if active_modal.modal.render_bare(cx) {
+            return active_modal.modal.view().into_any_element();
+        }
+
         div()
             .absolute()
             .size_full()
@@ -180,6 +193,12 @@ impl Render for ModalLayer {
                 background.fade_out(0.2);
                 this.bg(background)
             })
+            .on_mouse_down(
+                MouseButton::Left,
+                cx.listener(|this, _, window, cx| {
+                    this.hide_modal(window, cx);
+                }),
+            )
             .child(
                 v_flex()
                     .h(px(0.0))
@@ -195,5 +214,6 @@ impl Render for ModalLayer {
                             }),
                     ),
             )
+            .into_any_element()
     }
 }

crates/workspace/src/pane.rs πŸ”—

@@ -625,11 +625,11 @@ impl Pane {
                     self.last_focus_handle_by_item.get(&active_item.item_id())
                     && let Some(focus_handle) = weak_last_focus_handle.upgrade()
                 {
-                    focus_handle.focus(window);
+                    focus_handle.focus(window, cx);
                     return;
                 }
 
-                active_item.item_focus_handle(cx).focus(window);
+                active_item.item_focus_handle(cx).focus(window, cx);
             } else if let Some(focused) = window.focused(cx)
                 && !self.context_menu_focused(window, cx)
             {
@@ -638,7 +638,7 @@ impl Pane {
             }
         } else if let Some(welcome_page) = self.welcome_page.as_ref() {
             if self.focus_handle.is_focused(window) {
-                welcome_page.read(cx).focus_handle(cx).focus(window);
+                welcome_page.read(cx).focus_handle(cx).focus(window, cx);
             }
         }
     }
@@ -1999,7 +1999,7 @@ impl Pane {
 
             let should_activate = activate_pane || self.has_focus(window, cx);
             if self.items.len() == 1 && should_activate {
-                self.focus_handle.focus(window);
+                self.focus_handle.focus(window, cx);
             } else {
                 self.activate_item(
                     index_to_activate,
@@ -2350,7 +2350,7 @@ impl Pane {
     pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         if let Some(active_item) = self.active_item() {
             let focus_handle = active_item.item_focus_handle(cx);
-            window.focus(&focus_handle);
+            window.focus(&focus_handle, cx);
         }
     }
 

crates/workspace/src/persistence.rs πŸ”—

@@ -9,20 +9,26 @@ use std::{
 };
 
 use anyhow::{Context as _, Result, bail};
-use collections::{HashMap, IndexSet};
+use collections::{HashMap, HashSet, IndexSet};
 use db::{
+    kvp::KEY_VALUE_STORE,
     query,
     sqlez::{connection::Connection, domain::Domain},
     sqlez_macros::sql,
 };
-use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
-use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
+use gpui::{Axis, Bounds, Entity, Task, WindowBounds, WindowId, point, size};
+use project::{
+    debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
+    trusted_worktrees::{PathTrust, RemoteHostLocation, find_worktree_in_store},
+    worktree_store::WorktreeStore,
+};
 
 use language::{LanguageName, Toolchain, ToolchainScope};
 use project::WorktreeId;
 use remote::{
     DockerConnectionOptions, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
 };
+use serde::{Deserialize, Serialize};
 use sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
@@ -46,6 +52,11 @@ use model::{
 
 use self::model::{DockStructure, SerializedWorkspaceLocation};
 
+// https://www.sqlite.org/limits.html
+// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
+// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
+const MAX_QUERY_PLACEHOLDERS: usize = 32000;
+
 #[derive(Copy, Clone, Debug, PartialEq)]
 pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
 impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
@@ -154,6 +165,124 @@ impl Column for SerializedWindowBounds {
     }
 }
 
+const DEFAULT_WINDOW_BOUNDS_KEY: &str = "default_window_bounds";
+
+pub fn read_default_window_bounds() -> Option<(Uuid, WindowBounds)> {
+    let json_str = KEY_VALUE_STORE
+        .read_kvp(DEFAULT_WINDOW_BOUNDS_KEY)
+        .log_err()
+        .flatten()?;
+
+    let (display_uuid, persisted) =
+        serde_json::from_str::<(Uuid, WindowBoundsJson)>(&json_str).ok()?;
+    Some((display_uuid, persisted.into()))
+}
+
+pub async fn write_default_window_bounds(
+    bounds: WindowBounds,
+    display_uuid: Uuid,
+) -> anyhow::Result<()> {
+    let persisted = WindowBoundsJson::from(bounds);
+    let json_str = serde_json::to_string(&(display_uuid, persisted))?;
+    KEY_VALUE_STORE
+        .write_kvp(DEFAULT_WINDOW_BOUNDS_KEY.to_string(), json_str)
+        .await?;
+    Ok(())
+}
+
+#[derive(Serialize, Deserialize)]
+pub enum WindowBoundsJson {
+    Windowed {
+        x: i32,
+        y: i32,
+        width: i32,
+        height: i32,
+    },
+    Maximized {
+        x: i32,
+        y: i32,
+        width: i32,
+        height: i32,
+    },
+    Fullscreen {
+        x: i32,
+        y: i32,
+        width: i32,
+        height: i32,
+    },
+}
+
+impl From<WindowBounds> for WindowBoundsJson {
+    fn from(b: WindowBounds) -> Self {
+        match b {
+            WindowBounds::Windowed(bounds) => {
+                let origin = bounds.origin;
+                let size = bounds.size;
+                WindowBoundsJson::Windowed {
+                    x: f32::from(origin.x).round() as i32,
+                    y: f32::from(origin.y).round() as i32,
+                    width: f32::from(size.width).round() as i32,
+                    height: f32::from(size.height).round() as i32,
+                }
+            }
+            WindowBounds::Maximized(bounds) => {
+                let origin = bounds.origin;
+                let size = bounds.size;
+                WindowBoundsJson::Maximized {
+                    x: f32::from(origin.x).round() as i32,
+                    y: f32::from(origin.y).round() as i32,
+                    width: f32::from(size.width).round() as i32,
+                    height: f32::from(size.height).round() as i32,
+                }
+            }
+            WindowBounds::Fullscreen(bounds) => {
+                let origin = bounds.origin;
+                let size = bounds.size;
+                WindowBoundsJson::Fullscreen {
+                    x: f32::from(origin.x).round() as i32,
+                    y: f32::from(origin.y).round() as i32,
+                    width: f32::from(size.width).round() as i32,
+                    height: f32::from(size.height).round() as i32,
+                }
+            }
+        }
+    }
+}
+
+impl From<WindowBoundsJson> for WindowBounds {
+    fn from(n: WindowBoundsJson) -> Self {
+        match n {
+            WindowBoundsJson::Windowed {
+                x,
+                y,
+                width,
+                height,
+            } => WindowBounds::Windowed(Bounds {
+                origin: point(px(x as f32), px(y as f32)),
+                size: size(px(width as f32), px(height as f32)),
+            }),
+            WindowBoundsJson::Maximized {
+                x,
+                y,
+                width,
+                height,
+            } => WindowBounds::Maximized(Bounds {
+                origin: point(px(x as f32), px(y as f32)),
+                size: size(px(width as f32), px(height as f32)),
+            }),
+            WindowBoundsJson::Fullscreen {
+                x,
+                y,
+                width,
+                height,
+            } => WindowBounds::Fullscreen(Bounds {
+                origin: point(px(x as f32), px(y as f32)),
+                size: size(px(width as f32), px(height as f32)),
+            }),
+        }
+    }
+}
+
 #[derive(Debug)]
 pub struct Breakpoint {
     pub position: u32,
@@ -708,6 +837,14 @@ impl Domain for WorkspaceDb {
             ALTER TABLE remote_connections ADD COLUMN name TEXT;
             ALTER TABLE remote_connections ADD COLUMN container_id TEXT;
         ),
+        sql!(
+            CREATE TABLE IF NOT EXISTS trusted_worktrees (
+                trust_id INTEGER PRIMARY KEY AUTOINCREMENT,
+                absolute_path TEXT,
+                user_name TEXT,
+                host_name TEXT
+            ) STRICT;
+        ),
     ];
 
     // Allow recovering from bad migration that was initially shipped to nightly
@@ -1136,7 +1273,7 @@ impl WorkspaceDb {
         match options {
             RemoteConnectionOptions::Ssh(options) => {
                 kind = RemoteConnectionKind::Ssh;
-                host = Some(options.host);
+                host = Some(options.host.to_string());
                 port = options.port;
                 user = options.username;
             }
@@ -1349,7 +1486,7 @@ impl WorkspaceDb {
                 user: user,
             })),
             RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
-                host: host?,
+                host: host?.into(),
                 port,
                 username: user,
                 ..Default::default()
@@ -1364,24 +1501,6 @@ impl WorkspaceDb {
         }
     }
 
-    pub(crate) fn last_window(
-        &self,
-    ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
-        let mut prepared_query =
-            self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
-                SELECT
-                display,
-                window_state, window_x, window_y, window_width, window_height
-                FROM workspaces
-                WHERE paths
-                IS NOT NULL
-                ORDER BY timestamp DESC
-                LIMIT 1
-            ))?;
-        let result = prepared_query()?;
-        Ok(result.into_iter().next().unwrap_or((None, None)))
-    }
-
     query! {
         pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
             DELETE FROM workspaces
@@ -1796,6 +1915,135 @@ impl WorkspaceDb {
             Ok(())
         }).await
     }
+
+    pub(crate) async fn save_trusted_worktrees(
+        &self,
+        trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
+    ) -> anyhow::Result<()> {
+        use anyhow::Context as _;
+        use db::sqlez::statement::Statement;
+        use itertools::Itertools as _;
+
+        DB.clear_trusted_worktrees()
+            .await
+            .context("clearing previous trust state")?;
+
+        let trusted_worktrees = trusted_worktrees
+            .into_iter()
+            .flat_map(|(host, abs_paths)| {
+                abs_paths
+                    .into_iter()
+                    .map(move |abs_path| (Some(abs_path), host.clone()))
+            })
+            .collect::<Vec<_>>();
+        let mut first_worktree;
+        let mut last_worktree = 0_usize;
+        for (count, placeholders) in std::iter::once("(?, ?, ?)")
+            .cycle()
+            .take(trusted_worktrees.len())
+            .chunks(MAX_QUERY_PLACEHOLDERS / 3)
+            .into_iter()
+            .map(|chunk| {
+                let mut count = 0;
+                let placeholders = chunk
+                    .inspect(|_| {
+                        count += 1;
+                    })
+                    .join(", ");
+                (count, placeholders)
+            })
+            .collect::<Vec<_>>()
+        {
+            first_worktree = last_worktree;
+            last_worktree = last_worktree + count;
+            let query = format!(
+                r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
+VALUES {placeholders};"#
+            );
+
+            let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
+            self.write(move |conn| {
+                let mut statement = Statement::prepare(conn, query)?;
+                let mut next_index = 1;
+                for (abs_path, host) in trusted_worktrees {
+                    let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
+                    next_index = statement.bind(
+                        &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
+                        next_index,
+                    )?;
+                    next_index = statement.bind(
+                        &host
+                            .as_ref()
+                            .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
+                        next_index,
+                    )?;
+                    next_index = statement.bind(
+                        &host.as_ref().map(|host| host.host_identifier.as_str()),
+                        next_index,
+                    )?;
+                }
+                statement.exec()
+            })
+            .await
+            .context("inserting new trusted state")?;
+        }
+        Ok(())
+    }
+
+    pub fn fetch_trusted_worktrees(
+        &self,
+        worktree_store: Option<Entity<WorktreeStore>>,
+        host: Option<RemoteHostLocation>,
+        cx: &App,
+    ) -> Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
+        let trusted_worktrees = DB.trusted_worktrees()?;
+        Ok(trusted_worktrees
+            .into_iter()
+            .filter_map(|(abs_path, user_name, host_name)| {
+                let db_host = match (user_name, host_name) {
+                    (_, None) => None,
+                    (None, Some(host_name)) => Some(RemoteHostLocation {
+                        user_name: None,
+                        host_identifier: SharedString::new(host_name),
+                    }),
+                    (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
+                        user_name: Some(SharedString::new(user_name)),
+                        host_identifier: SharedString::new(host_name),
+                    }),
+                };
+
+                let abs_path = abs_path?;
+                Some(if db_host != host {
+                    (db_host, PathTrust::AbsPath(abs_path))
+                } else if let Some(worktree_store) = &worktree_store {
+                    find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
+                        .map(PathTrust::Worktree)
+                        .map(|trusted_worktree| (host.clone(), trusted_worktree))
+                        .unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path)))
+                } else {
+                    (db_host, PathTrust::AbsPath(abs_path))
+                })
+            })
+            .fold(HashMap::default(), |mut acc, (remote_host, path_trust)| {
+                acc.entry(remote_host)
+                    .or_insert_with(HashSet::default)
+                    .insert(path_trust);
+                acc
+            }))
+    }
+
+    query! {
+        fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
+            SELECT absolute_path, user_name, host_name
+            FROM trusted_worktrees
+        }
+    }
+
+    query! {
+        pub async fn clear_trusted_worktrees() -> Result<()> {
+            DELETE FROM trusted_worktrees
+        }
+    }
 }
 
 pub fn delete_unloaded_items(
@@ -2503,7 +2751,7 @@ mod tests {
 
         let connection_id = db
             .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
-                host: "my-host".to_string(),
+                host: "my-host".into(),
                 port: Some(1234),
                 ..Default::default()
             }))
@@ -2692,7 +2940,7 @@ mod tests {
         .into_iter()
         .map(|(host, user)| async {
             let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
-                host: host.to_string(),
+                host: host.into(),
                 username: Some(user.to_string()),
                 ..Default::default()
             });
@@ -2783,7 +3031,7 @@ mod tests {
 
         let connection_id = db
             .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
-                host: host.clone(),
+                host: host.clone().into(),
                 port,
                 username: user.clone(),
                 ..Default::default()
@@ -2794,7 +3042,7 @@ mod tests {
         // Test that calling the function again with the same parameters returns the same project
         let same_connection = db
             .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
-                host: host.clone(),
+                host: host.clone().into(),
                 port,
                 username: user.clone(),
                 ..Default::default()
@@ -2811,7 +3059,7 @@ mod tests {
 
         let different_connection = db
             .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
-                host: host2.clone(),
+                host: host2.clone().into(),
                 port: port2,
                 username: user2.clone(),
                 ..Default::default()
@@ -2830,7 +3078,7 @@ mod tests {
 
         let connection_id = db
             .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
-                host: host.clone(),
+                host: host.clone().into(),
                 port,
                 username: None,
                 ..Default::default()
@@ -2840,7 +3088,7 @@ mod tests {
 
         let same_connection_id = db
             .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
-                host: host.clone(),
+                host: host.clone().into(),
                 port,
                 username: user.clone(),
                 ..Default::default()
@@ -2870,7 +3118,7 @@ mod tests {
             ids.push(
                 db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
                     SshConnectionOptions {
-                        host: host.clone(),
+                        host: host.clone().into(),
                         port: *port,
                         username: user.clone(),
                         ..Default::default()
@@ -3048,4 +3296,53 @@ mod tests {
 
         assert_eq!(workspace.center_group, new_workspace.center_group);
     }
+
+    #[gpui::test]
+    async fn test_empty_workspace_window_bounds() {
+        zlog::init_test();
+
+        let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
+        let id = db.next_id().await.unwrap();
+
+        // Create a workspace with empty paths (empty workspace)
+        let empty_paths: &[&str] = &[];
+        let display_uuid = Uuid::new_v4();
+        let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
+            origin: point(px(100.0), px(200.0)),
+            size: size(px(800.0), px(600.0)),
+        }));
+
+        let workspace = SerializedWorkspace {
+            id,
+            paths: PathList::new(empty_paths),
+            location: SerializedWorkspaceLocation::Local,
+            center_group: Default::default(),
+            window_bounds: None,
+            display: None,
+            docks: Default::default(),
+            breakpoints: Default::default(),
+            centered_layout: false,
+            session_id: None,
+            window_id: None,
+            user_toolchains: Default::default(),
+        };
+
+        // Save the workspace (this creates the record with empty paths)
+        db.save_workspace(workspace.clone()).await;
+
+        // Save window bounds separately (as the actual code does via set_window_open_status)
+        db.set_window_open_status(id, window_bounds, display_uuid)
+            .await
+            .unwrap();
+
+        // Retrieve it using empty paths
+        let retrieved = db.workspace_for_roots(empty_paths).unwrap();
+
+        // Verify window bounds were persisted
+        assert_eq!(retrieved.id, id);
+        assert!(retrieved.window_bounds.is_some());
+        assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
+        assert!(retrieved.display.is_some());
+        assert_eq!(retrieved.display.unwrap(), display_uuid);
+    }
 }

crates/workspace/src/security_modal.rs πŸ”—

@@ -23,7 +23,7 @@ use ui::{
 use crate::{DismissDecision, ModalView, ToggleWorktreeSecurity};
 
 pub struct SecurityModal {
-    restricted_paths: HashMap<Option<WorktreeId>, RestrictedPath>,
+    restricted_paths: HashMap<WorktreeId, RestrictedPath>,
     home_dir: Option<PathBuf>,
     trust_parents: bool,
     worktree_store: WeakEntity<WorktreeStore>,
@@ -34,7 +34,7 @@ pub struct SecurityModal {
 
 #[derive(Debug, PartialEq, Eq)]
 struct RestrictedPath {
-    abs_path: Option<Arc<Path>>,
+    abs_path: Arc<Path>,
     is_file: bool,
     host: Option<RemoteHostLocation>,
 }
@@ -102,48 +102,31 @@ impl Render for SecurityModal {
                             .child(Icon::new(IconName::Warning).color(Color::Warning))
                             .child(Label::new(header_label)),
                     )
-                    .children(self.restricted_paths.values().map(|restricted_path| {
-                        let abs_path = restricted_path.abs_path.as_ref().and_then(|abs_path| {
-                            if restricted_path.is_file {
-                                abs_path.parent()
-                            } else {
-                                Some(abs_path.as_ref())
-                            }
-                        });
-
-                        let label = match abs_path {
-                            Some(abs_path) => match &restricted_path.host {
-                                Some(remote_host) => match &remote_host.user_name {
-                                    Some(user_name) => format!(
-                                        "{} ({}@{})",
-                                        self.shorten_path(abs_path).display(),
-                                        user_name,
-                                        remote_host.host_identifier
-                                    ),
-                                    None => format!(
-                                        "{} ({})",
-                                        self.shorten_path(abs_path).display(),
-                                        remote_host.host_identifier
-                                    ),
-                                },
-                                None => self.shorten_path(abs_path).display().to_string(),
-                            },
-                            None => match &restricted_path.host {
-                                Some(remote_host) => match &remote_host.user_name {
-                                    Some(user_name) => format!(
-                                        "Empty project ({}@{})",
-                                        user_name, remote_host.host_identifier
-                                    ),
-                                    None => {
-                                        format!("Empty project ({})", remote_host.host_identifier)
-                                    }
-                                },
-                                None => "Empty project".to_string(),
+                    .children(self.restricted_paths.values().filter_map(|restricted_path| {
+                        let abs_path = if restricted_path.is_file {
+                            restricted_path.abs_path.parent()
+                        } else {
+                            Some(restricted_path.abs_path.as_ref())
+                        }?;
+                        let label = match &restricted_path.host {
+                            Some(remote_host) => match &remote_host.user_name {
+                                Some(user_name) => format!(
+                                    "{} ({}@{})",
+                                    self.shorten_path(abs_path).display(),
+                                    user_name,
+                                    remote_host.host_identifier
+                                ),
+                                None => format!(
+                                    "{} ({})",
+                                    self.shorten_path(abs_path).display(),
+                                    remote_host.host_identifier
+                                ),
                             },
+                            None => self.shorten_path(abs_path).display().to_string(),
                         };
-                        h_flex()
+                        Some(h_flex()
                             .pl(IconSize::default().rems() + rems(0.5))
-                            .child(Label::new(label).color(Color::Muted))
+                            .child(Label::new(label).color(Color::Muted)))
                     })),
             )
             .child(
@@ -254,7 +237,7 @@ impl SecurityModal {
                 has_restricted_files |= restricted_path.is_file;
                 !restricted_path.is_file
             })
-            .filter_map(|restricted_path| restricted_path.abs_path.as_ref()?.parent())
+            .filter_map(|restricted_path| restricted_path.abs_path.parent())
             .collect::<SmallVec<[_; 2]>>();
         match available_parents.len() {
             0 => {
@@ -265,8 +248,8 @@ impl SecurityModal {
                 }
             }
             1 => Some(Cow::Owned(format!(
-                "Trust all projects in the {:?} folder",
-                self.shorten_path(available_parents[0])
+                "Trust all projects in the {:} folder",
+                self.shorten_path(available_parents[0]).display()
             ))),
             _ => Some(Cow::Borrowed("Trust all projects in the parent folders")),
         }
@@ -289,19 +272,17 @@ impl SecurityModal {
                 let mut paths_to_trust = self
                     .restricted_paths
                     .keys()
-                    .map(|worktree_id| match worktree_id {
-                        Some(worktree_id) => PathTrust::Worktree(*worktree_id),
-                        None => PathTrust::Workspace,
-                    })
+                    .copied()
+                    .map(PathTrust::Worktree)
                     .collect::<HashSet<_>>();
                 if self.trust_parents {
                     paths_to_trust.extend(self.restricted_paths.values().filter_map(
                         |restricted_paths| {
                             if restricted_paths.is_file {
-                                Some(PathTrust::Workspace)
+                                None
                             } else {
                                 let parent_abs_path =
-                                    restricted_paths.abs_path.as_ref()?.parent()?.to_owned();
+                                    restricted_paths.abs_path.parent()?.to_owned();
                                 Some(PathTrust::AbsPath(parent_abs_path))
                             }
                         },
@@ -322,42 +303,22 @@ impl SecurityModal {
     pub fn refresh_restricted_paths(&mut self, cx: &mut Context<Self>) {
         if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
             if let Some(worktree_store) = self.worktree_store.upgrade() {
-                let mut new_restricted_worktrees = trusted_worktrees
+                let new_restricted_worktrees = trusted_worktrees
                     .read(cx)
-                    .restricted_worktrees(worktree_store.read(cx), self.remote_host.clone(), cx)
+                    .restricted_worktrees(worktree_store.read(cx), cx)
                     .into_iter()
-                    .filter_map(|restricted_path| {
-                        let restricted_path = match restricted_path {
-                            Some((worktree_id, abs_path)) => {
-                                let worktree =
-                                    worktree_store.read(cx).worktree_for_id(worktree_id, cx)?;
-                                (
-                                    Some(worktree_id),
-                                    RestrictedPath {
-                                        abs_path: Some(abs_path),
-                                        is_file: worktree.read(cx).is_single_file(),
-                                        host: self.remote_host.clone(),
-                                    },
-                                )
-                            }
-                            None => (
-                                None,
-                                RestrictedPath {
-                                    abs_path: None,
-                                    is_file: false,
-                                    host: self.remote_host.clone(),
-                                },
-                            ),
-                        };
-                        Some(restricted_path)
+                    .filter_map(|(worktree_id, abs_path)| {
+                        let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?;
+                        Some((
+                            worktree_id,
+                            RestrictedPath {
+                                abs_path,
+                                is_file: worktree.read(cx).is_single_file(),
+                                host: self.remote_host.clone(),
+                            },
+                        ))
                     })
                     .collect::<HashMap<_, _>>();
-                // Do not clutter the UI:
-                // * trusting regular local worktrees assumes the workspace is trusted either, on the same host.
-                // * trusting a workspace trusts all single-file worktrees on the same host.
-                if new_restricted_worktrees.len() > 1 {
-                    new_restricted_worktrees.remove(&None);
-                }
 
                 if self.restricted_paths != new_restricted_worktrees {
                     self.trust_parents = false;

crates/workspace/src/welcome.rs πŸ”—

@@ -250,12 +250,12 @@ impl WelcomePage {
     }
 
     fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
-        window.focus_next();
+        window.focus_next(cx);
         cx.notify();
     }
 
     fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
-        window.focus_prev();
+        window.focus_prev(cx);
         cx.notify();
     }
 

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

@@ -80,7 +80,7 @@ use project::{
     debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
     project_settings::ProjectSettings,
     toolchain_store::ToolchainStoreEvent,
-    trusted_worktrees::TrustedWorktrees,
+    trusted_worktrees::{TrustedWorktrees, TrustedWorktreesEvent},
 };
 use remote::{
     RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
@@ -135,7 +135,9 @@ pub use workspace_settings::{
 use zed_actions::{Spawn, feedback::FileBugReport};
 
 use crate::{
-    item::ItemBufferKind, notifications::NotificationId, utility_pane::UTILITY_PANE_MIN_WIDTH,
+    item::ItemBufferKind,
+    notifications::NotificationId,
+    utility_pane::{UTILITY_PANE_MIN_WIDTH, utility_slot_for_dock_position},
 };
 use crate::{
     persistence::{
@@ -986,6 +988,7 @@ impl AppState {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut App) -> Arc<Self> {
+        use fs::Fs;
         use node_runtime::NodeRuntime;
         use session::Session;
         use settings::SettingsStore;
@@ -996,6 +999,7 @@ impl AppState {
         }
 
         let fs = fs::FakeFs::new(cx.background_executor().clone());
+        <dyn Fs>::set_global(fs.clone(), cx);
         let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
         let clock = Arc::new(clock::FakeSystemClock::new());
         let http_client = http_client::FakeHttpClient::with_404_response();
@@ -1184,6 +1188,7 @@ pub struct Workspace {
     _observe_current_user: Task<Result<()>>,
     _schedule_serialize_workspace: Option<Task<()>>,
     _schedule_serialize_ssh_paths: Option<Task<()>>,
+    _schedule_serialize_worktree_trust: Task<()>,
     pane_history_timestamp: Arc<AtomicUsize>,
     bounds: Bounds<Pixels>,
     pub centered_layout: bool,
@@ -1229,16 +1234,40 @@ impl Workspace {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
-        cx.observe_global::<SettingsStore>(|_, cx| {
-            if ProjectSettings::get_global(cx).session.trust_all_worktrees {
-                if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
-                    trusted_worktrees.update(cx, |trusted_worktrees, cx| {
-                        trusted_worktrees.auto_trust_all(cx);
-                    })
+        if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
+            cx.subscribe(&trusted_worktrees, |workspace, worktrees_store, e, cx| {
+                if let TrustedWorktreesEvent::Trusted(..) = e {
+                    // Do not persist auto trusted worktrees
+                    if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
+                        let new_trusted_worktrees =
+                            worktrees_store.update(cx, |worktrees_store, cx| {
+                                worktrees_store.trusted_paths_for_serialization(cx)
+                            });
+                        let timeout = cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
+                        workspace._schedule_serialize_worktree_trust =
+                            cx.background_spawn(async move {
+                                timeout.await;
+                                persistence::DB
+                                    .save_trusted_worktrees(new_trusted_worktrees)
+                                    .await
+                                    .log_err();
+                            });
+                    }
                 }
-            }
-        })
-        .detach();
+            })
+            .detach();
+
+            cx.observe_global::<SettingsStore>(|_, cx| {
+                if ProjectSettings::get_global(cx).session.trust_all_worktrees {
+                    if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
+                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
+                            trusted_worktrees.auto_trust_all(cx);
+                        })
+                    }
+                }
+            })
+            .detach();
+        }
 
         cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
             match event {
@@ -1250,11 +1279,25 @@ impl Workspace {
                     this.collaborator_left(*peer_id, window, cx);
                 }
 
-                project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
-                    this.update_window_title(window, cx);
-                    this.serialize_workspace(window, cx);
-                    // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`.
-                    this.update_history(cx);
+                project::Event::WorktreeUpdatedEntries(worktree_id, _) => {
+                    if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
+                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
+                            trusted_worktrees.can_trust(*worktree_id, cx);
+                        });
+                    }
+                }
+
+                project::Event::WorktreeRemoved(_) => {
+                    this.update_worktree_data(window, cx);
+                }
+
+                project::Event::WorktreeAdded(worktree_id) => {
+                    if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
+                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
+                            trusted_worktrees.can_trust(*worktree_id, cx);
+                        });
+                    }
+                    this.update_worktree_data(window, cx);
                 }
 
                 project::Event::DisconnectedFromHost => {
@@ -1351,7 +1394,7 @@ impl Workspace {
 
         cx.on_focus_lost(window, |this, window, cx| {
             let focus_handle = this.focus_handle(cx);
-            window.focus(&focus_handle);
+            window.focus(&focus_handle, cx);
         })
         .detach();
 
@@ -1375,7 +1418,7 @@ impl Workspace {
         cx.subscribe_in(&center_pane, window, Self::handle_pane_event)
             .detach();
 
-        window.focus(&center_pane.focus_handle(cx));
+        window.focus(&center_pane.focus_handle(cx), cx);
 
         cx.emit(Event::PaneAdded(center_pane.clone()));
 
@@ -1466,6 +1509,15 @@ impl Workspace {
                             && let Ok(display_uuid) = display.uuid()
                         {
                             let window_bounds = window.inner_window_bounds();
+                            let has_paths = !this.root_paths(cx).is_empty();
+                            if !has_paths {
+                                cx.background_executor()
+                                    .spawn(persistence::write_default_window_bounds(
+                                        window_bounds,
+                                        display_uuid,
+                                    ))
+                                    .detach_and_log_err(cx);
+                            }
                             if let Some(database_id) = workspace_id {
                                 cx.background_executor()
                                     .spawn(DB.set_window_open_status(
@@ -1474,6 +1526,13 @@ impl Workspace {
                                         display_uuid,
                                     ))
                                     .detach_and_log_err(cx);
+                            } else {
+                                cx.background_executor()
+                                    .spawn(persistence::write_default_window_bounds(
+                                        window_bounds,
+                                        display_uuid,
+                                    ))
+                                    .detach_and_log_err(cx);
                             }
                         }
                         this.bounds_save_task_queued.take();
@@ -1540,6 +1599,7 @@ impl Workspace {
             _apply_leader_updates,
             _schedule_serialize_workspace: None,
             _schedule_serialize_ssh_paths: None,
+            _schedule_serialize_worktree_trust: Task::ready(()),
             leader_updates_tx,
             _subscriptions: subscriptions,
             pane_history_timestamp,
@@ -1568,6 +1628,7 @@ impl Workspace {
         app_state: Arc<AppState>,
         requesting_window: Option<WindowHandle<Workspace>>,
         env: Option<HashMap<String, String>>,
+        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
         cx: &mut App,
     ) -> Task<
         anyhow::Result<(
@@ -1675,6 +1736,12 @@ impl Workspace {
                         );
 
                         workspace.centered_layout = centered_layout;
+
+                        // Call init callback to add items before window renders
+                        if let Some(init) = init {
+                            init(&mut workspace, window, cx);
+                        }
+
                         workspace
                     });
                 })?;
@@ -1684,15 +1751,15 @@ impl Workspace {
 
                 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
                     (Some(WindowBounds::Windowed(bounds)), None)
-                } else if let Some(workspace) = serialized_workspace.as_ref() {
+                } else if let Some(workspace) = serialized_workspace.as_ref()
+                    && let Some(display) = workspace.display
+                    && let Some(bounds) = workspace.window_bounds.as_ref()
+                {
                     // Reopening an existing workspace - restore its saved bounds
-                    if let (Some(display), Some(bounds)) =
-                        (workspace.display, workspace.window_bounds.as_ref())
-                    {
-                        (Some(bounds.0), Some(display))
-                    } else {
-                        (None, None)
-                    }
+                    (Some(bounds.0), Some(display))
+                } else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
+                    // New or empty workspace - use the last known window bounds
+                    (Some(bounds), Some(display))
                 } else {
                     // New window - let GPUI's default_bounds() handle cascading
                     (None, None)
@@ -1718,6 +1785,12 @@ impl Workspace {
                                 cx,
                             );
                             workspace.centered_layout = centered_layout;
+
+                            // Call init callback to add items before window renders
+                            if let Some(init) = init {
+                                init(&mut workspace, window, cx);
+                            }
+
                             workspace
                         })
                     }
@@ -1813,10 +1886,18 @@ impl Workspace {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let mut found_in_dock = None;
         for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
-            dock.update(cx, |dock, cx| {
-                dock.remove_panel(panel, window, cx);
-            })
+            let found = dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
+
+            if found {
+                found_in_dock = Some(dock.clone());
+            }
+        }
+        if let Some(found_in_dock) = found_in_dock {
+            let position = found_in_dock.read(cx).position();
+            let slot = utility_slot_for_dock_position(position);
+            self.clear_utility_pane_if_provider(slot, Entity::entity_id(panel), cx);
         }
     }
 
@@ -1977,7 +2058,7 @@ impl Workspace {
     ) -> Task<Result<()>> {
         let to_load = if let Some(pane) = pane.upgrade() {
             pane.update(cx, |pane, cx| {
-                window.focus(&pane.focus_handle(cx));
+                window.focus(&pane.focus_handle(cx), cx);
                 loop {
                     // Retrieve the weak item handle from the history.
                     let entry = pane.nav_history_mut().pop(mode, cx)?;
@@ -2283,7 +2364,7 @@ impl Workspace {
             Task::ready(Ok(callback(self, window, cx)))
         } else {
             let env = self.project.read(cx).cli_environment(cx);
-            let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx);
+            let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx);
             cx.spawn_in(window, async move |_vh, cx| {
                 let (workspace, _) = task.await?;
                 workspace.update(cx, callback)
@@ -3096,7 +3177,7 @@ impl Workspace {
                     }
                 } else {
                     let focus_handle = &active_panel.panel_focus_handle(cx);
-                    window.focus(focus_handle);
+                    window.focus(focus_handle, cx);
                     reveal_dock = true;
                 }
             }
@@ -3108,7 +3189,7 @@ impl Workspace {
 
         if focus_center {
             self.active_pane
-                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
+                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
         }
 
         cx.notify();
@@ -3276,7 +3357,7 @@ impl Workspace {
                     if let Some(panel) = panel.as_ref() {
                         if should_focus(&**panel, window, cx) {
                             dock.set_open(true, window, cx);
-                            panel.panel_focus_handle(cx).focus(window);
+                            panel.panel_focus_handle(cx).focus(window, cx);
                         } else {
                             focus_center = true;
                         }
@@ -3286,7 +3367,7 @@ impl Workspace {
 
                 if focus_center {
                     self.active_pane
-                        .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
+                        .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
                 }
 
                 result_panel = panel;
@@ -3360,7 +3441,7 @@ impl Workspace {
 
         if focus_center {
             self.active_pane
-                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
+                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
         }
 
         if self.zoomed_position != dock_to_reveal {
@@ -3391,7 +3472,7 @@ impl Workspace {
             .detach();
         self.panes.push(pane.clone());
 
-        window.focus(&pane.focus_handle(cx));
+        window.focus(&pane.focus_handle(cx), cx);
 
         cx.emit(Event::PaneAdded(pane.clone()));
         pane
@@ -3786,7 +3867,7 @@ impl Workspace {
     ) {
         let panes = self.center.panes();
         if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
-            window.focus(&pane.focus_handle(cx));
+            window.focus(&pane.focus_handle(cx), cx);
         } else {
             self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
                 .detach();
@@ -3856,7 +3937,7 @@ impl Workspace {
         if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
             let next_ix = (ix + 1) % panes.len();
             let next_pane = panes[next_ix].clone();
-            window.focus(&next_pane.focus_handle(cx));
+            window.focus(&next_pane.focus_handle(cx), cx);
         }
     }
 
@@ -3865,7 +3946,7 @@ impl Workspace {
         if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
             let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
             let prev_pane = panes[prev_ix].clone();
-            window.focus(&prev_pane.focus_handle(cx));
+            window.focus(&prev_pane.focus_handle(cx), cx);
         }
     }
 
@@ -3961,7 +4042,7 @@ impl Workspace {
             Some(ActivateInDirectionTarget::Pane(pane)) => {
                 let pane = pane.read(cx);
                 if let Some(item) = pane.active_item() {
-                    item.item_focus_handle(cx).focus(window);
+                    item.item_focus_handle(cx).focus(window, cx);
                 } else {
                     log::error!(
                         "Could not find a focus target when in switching focus in {direction} direction for a pane",
@@ -3973,7 +4054,7 @@ impl Workspace {
                 window.defer(cx, move |window, cx| {
                     let dock = dock.read(cx);
                     if let Some(panel) = dock.active_panel() {
-                        panel.panel_focus_handle(cx).focus(window);
+                        panel.panel_focus_handle(cx).focus(window, cx);
                     } else {
                         log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
                     }
@@ -4593,7 +4674,7 @@ impl Workspace {
 
         // if you're already following, find the right pane and focus it.
         if let Some(follower_state) = self.follower_states.get(&leader_id) {
-            window.focus(&follower_state.pane().focus_handle(cx));
+            window.focus(&follower_state.pane().focus_handle(cx), cx);
 
             return;
         }
@@ -5405,12 +5486,12 @@ impl Workspace {
     ) {
         self.panes.retain(|p| p != pane);
         if let Some(focus_on) = focus_on {
-            focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
+            focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
         } else if self.active_pane() == pane {
             self.panes
                 .last()
                 .unwrap()
-                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
+                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
         }
         if self.last_active_center_pane == Some(pane.downgrade()) {
             self.last_active_center_pane = None;
@@ -5584,12 +5665,24 @@ impl Workspace {
                     persistence::DB.save_workspace(serialized_workspace).await;
                 })
             }
-            WorkspaceLocation::DetachFromSession => window.spawn(cx, async move |_| {
-                persistence::DB
-                    .set_session_id(database_id, None)
-                    .await
-                    .log_err();
-            }),
+            WorkspaceLocation::DetachFromSession => {
+                let window_bounds = SerializedWindowBounds(window.window_bounds());
+                let display = window.display(cx).and_then(|d| d.uuid().ok());
+                window.spawn(cx, async move |_| {
+                    persistence::DB
+                        .set_window_open_status(
+                            database_id,
+                            window_bounds,
+                            display.unwrap_or_default(),
+                        )
+                        .await
+                        .log_err();
+                    persistence::DB
+                        .set_session_id(database_id, None)
+                        .await
+                        .log_err();
+                })
+            }
             WorkspaceLocation::None => Task::ready(()),
         }
     }
@@ -5970,12 +6063,14 @@ impl Workspace {
             .on_action(
                 cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
                     if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
-                        let clear_task = trusted_worktrees.update(cx, |trusted_worktrees, cx| {
-                            trusted_worktrees.clear_trusted_paths(cx)
+                        trusted_worktrees.update(cx, |trusted_worktrees, _| {
+                            trusted_worktrees.clear_trusted_paths()
                         });
+                        let clear_task = persistence::DB.clear_trusted_worktrees();
                         cx.spawn(async move |_, cx| {
-                            clear_task.await;
-                            cx.update(|cx| reload(cx)).ok();
+                            if clear_task.await.log_err().is_some() {
+                                cx.update(|cx| reload(cx)).ok();
+                            }
                         })
                         .detach();
                     }
@@ -6166,7 +6261,7 @@ impl Workspace {
         let workspace = Self::new(Default::default(), project, app_state, window, cx);
         workspace
             .active_pane
-            .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
+            .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
         workspace
     }
 
@@ -6496,6 +6591,13 @@ impl Workspace {
             }
         }
     }
+
+    fn update_worktree_data(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) {
+        self.update_window_title(window, cx);
+        self.serialize_workspace(window, cx);
+        // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`.
+        self.update_history(cx);
+    }
 }
 
 fn leader_border_for_pane(
@@ -7682,7 +7784,14 @@ pub fn join_channel(
             // no open workspaces, make one to show the error in (blergh)
             let (window_handle, _) = cx
                 .update(|cx| {
-                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
+                    Workspace::new_local(
+                        vec![],
+                        app_state.clone(),
+                        requesting_window,
+                        None,
+                        None,
+                        cx,
+                    )
                 })?
                 .await?;
 
@@ -7748,7 +7857,7 @@ pub async fn get_any_active_workspace(
     // find an existing workspace to focus and show call controls
     let active_window = activate_any_workspace_window(&mut cx);
     if active_window.is_none() {
-        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
+        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, None, cx))?
             .await?;
     }
     activate_any_workspace_window(&mut cx).context("could not open zed")
@@ -7915,6 +8024,7 @@ pub fn open_paths(
                     app_state.clone(),
                     open_options.replace_window,
                     open_options.env,
+                    None,
                     cx,
                 )
             })?
@@ -7959,14 +8069,17 @@ pub fn open_new(
     cx: &mut App,
     init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
 ) -> Task<anyhow::Result<()>> {
-    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
-    cx.spawn(async move |cx| {
-        let (workspace, opened_paths) = task.await?;
-        workspace.update(cx, |workspace, window, cx| {
-            if opened_paths.is_empty() {
-                init(workspace, window, cx)
-            }
-        })?;
+    let task = Workspace::new_local(
+        Vec::new(),
+        app_state,
+        None,
+        open_options.env,
+        Some(Box::new(init)),
+        cx,
+    );
+    cx.spawn(async move |_cx| {
+        let (_workspace, _opened_paths) = task.await?;
+        // Init callback is called synchronously during workspace creation
         Ok(())
     })
 }
@@ -8622,7 +8735,7 @@ fn move_all_items(
         // This automatically removes duplicate items in the pane
         to_pane.update(cx, |destination, cx| {
             destination.add_item(item_handle, true, true, None, window, cx);
-            window.focus(&destination.focus_handle(cx))
+            window.focus(&destination.focus_handle(cx), cx)
         });
     }
 }
@@ -8666,7 +8779,7 @@ pub fn move_item(
             cx,
         );
         if activate {
-            window.focus(&destination.focus_handle(cx))
+            window.focus(&destination.focus_handle(cx), cx)
         }
     });
 }
@@ -8768,14 +8881,13 @@ pub fn remote_workspace_position_from_db(
         } else {
             let restorable_bounds = serialized_workspace
                 .as_ref()
-                .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
-                .or_else(|| {
-                    let (display, window_bounds) = DB.last_window().log_err()?;
-                    Some((display?, window_bounds?))
-                });
+                .and_then(|workspace| {
+                    Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
+                })
+                .or_else(|| persistence::read_default_window_bounds());
 
-            if let Some((serialized_display, serialized_status)) = restorable_bounds {
-                (Some(serialized_status.0), Some(serialized_display))
+            if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
+                (Some(serialized_bounds), Some(serialized_display))
             } else {
                 (None, None)
             }

crates/worktree/Cargo.toml πŸ”—

@@ -25,8 +25,10 @@ test-support = [
 [dependencies]
 anyhow.workspace = true
 async-lock.workspace = true
+chardetng.workspace = true
 clock.workspace = true
 collections.workspace = true
+encoding_rs.workspace = true
 fs.workspace = true
 futures.workspace = true
 fuzzy.workspace = true

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

@@ -5,8 +5,10 @@ mod worktree_tests;
 
 use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
 use anyhow::{Context as _, Result, anyhow};
+use chardetng::EncodingDetector;
 use clock::ReplicaId;
 use collections::{HashMap, HashSet, VecDeque};
+use encoding_rs::Encoding;
 use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items};
 use futures::{
     FutureExt as _, Stream, StreamExt,
@@ -105,6 +107,8 @@ pub enum CreatedEntry {
 pub struct LoadedFile {
     pub file: Arc<File>,
     pub text: String,
+    pub encoding: &'static Encoding,
+    pub has_bom: bool,
 }
 
 pub struct LoadedBinaryFile {
@@ -741,10 +745,14 @@ impl Worktree {
         path: Arc<RelPath>,
         text: Rope,
         line_ending: LineEnding,
+        encoding: &'static Encoding,
+        has_bom: bool,
         cx: &Context<Worktree>,
     ) -> Task<Result<Arc<File>>> {
         match self {
-            Worktree::Local(this) => this.write_file(path, text, line_ending, cx),
+            Worktree::Local(this) => {
+                this.write_file(path, text, line_ending, encoding, has_bom, cx)
+            }
             Worktree::Remote(_) => {
                 Task::ready(Err(anyhow!("remote worktree can't yet write files")))
             }
@@ -1351,7 +1359,9 @@ impl LocalWorktree {
                     anyhow::bail!("File is too large to load");
                 }
             }
-            let text = fs.load(&abs_path).await?;
+
+            let content = fs.load_bytes(&abs_path).await?;
+            let (text, encoding, has_bom) = decode_byte(content);
 
             let worktree = this.upgrade().context("worktree was dropped")?;
             let file = match entry.await? {
@@ -1379,7 +1389,12 @@ impl LocalWorktree {
                 }
             };
 
-            Ok(LoadedFile { file, text })
+            Ok(LoadedFile {
+                file,
+                text,
+                encoding,
+                has_bom,
+            })
         })
     }
 
@@ -1462,6 +1477,8 @@ impl LocalWorktree {
         path: Arc<RelPath>,
         text: Rope,
         line_ending: LineEnding,
+        encoding: &'static Encoding,
+        has_bom: bool,
         cx: &Context<Worktree>,
     ) -> Task<Result<Arc<File>>> {
         let fs = self.fs.clone();
@@ -1471,7 +1488,49 @@ impl LocalWorktree {
         let write = cx.background_spawn({
             let fs = fs.clone();
             let abs_path = abs_path.clone();
-            async move { fs.save(&abs_path, &text, line_ending).await }
+            async move {
+                let bom_bytes = if has_bom {
+                    if encoding == encoding_rs::UTF_16LE {
+                        vec![0xFF, 0xFE]
+                    } else if encoding == encoding_rs::UTF_16BE {
+                        vec![0xFE, 0xFF]
+                    } else if encoding == encoding_rs::UTF_8 {
+                        vec![0xEF, 0xBB, 0xBF]
+                    } else {
+                        vec![]
+                    }
+                } else {
+                    vec![]
+                };
+
+                // For UTF-8, use the optimized `fs.save` which writes Rope chunks directly to disk
+                // without allocating a contiguous string.
+                if encoding == encoding_rs::UTF_8 && !has_bom {
+                    return fs.save(&abs_path, &text, line_ending).await;
+                }
+                // For legacy encodings (e.g. Shift-JIS), we fall back to converting the entire Rope
+                // to a String/Bytes in memory before writing.
+                //
+                // Note: This is inefficient for very large files compared to the streaming approach above,
+                // but supporting streaming writes for arbitrary encodings would require a significant
+                // refactor of the `fs` crate to expose a Writer interface.
+                let text_string = text.to_string();
+                let normalized_text = match line_ending {
+                    LineEnding::Unix => text_string,
+                    LineEnding::Windows => text_string.replace('\n', "\r\n"),
+                };
+
+                let (cow, _, _) = encoding.encode(&normalized_text);
+                let bytes = if !bom_bytes.is_empty() {
+                    let mut bytes = bom_bytes;
+                    bytes.extend_from_slice(&cow);
+                    bytes.into()
+                } else {
+                    cow
+                };
+
+                fs.write(&abs_path, &bytes).await
+            }
         });
 
         cx.spawn(async move |this, cx| {
@@ -5782,3 +5841,40 @@ impl fs::Watcher for NullWatcher {
         Ok(())
     }
 }
+
+fn decode_byte(bytes: Vec<u8>) -> (String, &'static Encoding, bool) {
+    // check BOM
+    if let Some((encoding, _bom_len)) = Encoding::for_bom(&bytes) {
+        let (cow, _) = encoding.decode_with_bom_removal(&bytes);
+        return (cow.into_owned(), encoding, true);
+    }
+
+    fn detect_encoding(bytes: Vec<u8>) -> (String, &'static Encoding) {
+        let mut detector = EncodingDetector::new();
+        detector.feed(&bytes, true);
+
+        let encoding = detector.guess(None, true); // Use None for TLD hint to ensure neutral detection logic.
+
+        let (cow, _, _) = encoding.decode(&bytes);
+        (cow.into_owned(), encoding)
+    }
+
+    match String::from_utf8(bytes) {
+        Ok(text) => {
+            // ISO-2022-JP (and other ISO-2022 variants) consists entirely of 7-bit ASCII bytes,
+            // so it is valid UTF-8. However, it contains escape sequences starting with '\x1b'.
+            // If we find an escape character, we double-check the encoding to prevent
+            // displaying raw escape sequences instead of the correct characters.
+            if text.contains('\x1b') {
+                let (s, enc) = detect_encoding(text.into_bytes());
+                (s, enc, false)
+            } else {
+                (text, encoding_rs::UTF_8, false)
+            }
+        }
+        Err(e) => {
+            let (s, enc) = detect_encoding(e.into_bytes());
+            (s, enc, false)
+        }
+    }
+}

crates/worktree/src/worktree_tests.rs πŸ”—

@@ -1,5 +1,6 @@
 use crate::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle};
-use anyhow::Result;
+use anyhow::{Context as _, Result};
+use encoding_rs;
 use fs::{FakeFs, Fs, RealFs, RemoveOptions};
 use git::{DOT_GIT, GITIGNORE, REPO_EXCLUDE};
 use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};
@@ -19,6 +20,7 @@ use std::{
 };
 use util::{
     ResultExt, path,
+    paths::PathStyle,
     rel_path::{RelPath, rel_path},
     test::TempTree,
 };
@@ -723,6 +725,8 @@ async fn test_write_file(cx: &mut TestAppContext) {
                 rel_path("tracked-dir/file.txt").into(),
                 "hello".into(),
                 Default::default(),
+                encoding_rs::UTF_8,
+                false,
                 cx,
             )
         })
@@ -734,6 +738,8 @@ async fn test_write_file(cx: &mut TestAppContext) {
                 rel_path("ignored-dir/file.txt").into(),
                 "world".into(),
                 Default::default(),
+                encoding_rs::UTF_8,
+                false,
                 cx,
             )
         })
@@ -2035,8 +2041,14 @@ fn randomly_mutate_worktree(
                 })
             } else {
                 log::info!("overwriting file {:?} ({})", &entry.path, entry.id.0);
-                let task =
-                    worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
+                let task = worktree.write_file(
+                    entry.path.clone(),
+                    "".into(),
+                    Default::default(),
+                    encoding_rs::UTF_8,
+                    false,
+                    cx,
+                );
                 cx.background_spawn(async move {
                     task.await?;
                     Ok(())
@@ -2552,3 +2564,176 @@ fn init_test(cx: &mut gpui::TestAppContext) {
         cx.set_global(settings_store);
     });
 }
+
+#[gpui::test]
+async fn test_load_file_encoding(cx: &mut TestAppContext) {
+    init_test(cx);
+    let test_cases: Vec<(&str, &[u8], &str)> = vec![
+        ("utf8.txt", "こんにけは".as_bytes(), "こんにけは"), // "こんにけは" is Japanese "Hello"
+        (
+            "sjis.txt",
+            &[0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, 0xbf, 0x82, 0xcd],
+            "こんにけは",
+        ),
+        (
+            "eucjp.txt",
+            &[0xa4, 0xb3, 0xa4, 0xf3, 0xa4, 0xcb, 0xa4, 0xc1, 0xa4, 0xcf],
+            "こんにけは",
+        ),
+        (
+            "iso2022jp.txt",
+            &[
+                0x1b, 0x24, 0x42, 0x24, 0x33, 0x24, 0x73, 0x24, 0x4b, 0x24, 0x41, 0x24, 0x4f, 0x1b,
+                0x28, 0x42,
+            ],
+            "こんにけは",
+        ),
+        // Western Europe (Windows-1252)
+        // "CafΓ©" -> 0xE9 is 'Γ©' in Windows-1252 (it is typically 0xC3 0xA9 in UTF-8)
+        ("win1252.txt", &[0x43, 0x61, 0x66, 0xe9], "CafΓ©"),
+        // Chinese Simplified (GBK)
+        // Note: We use a slightly longer string here because short byte sequences can be ambiguous
+        // in multi-byte encodings. Providing more context helps the heuristic detector guess correctly.
+        // Text: "δ»Šε€©ε€©ζ°”δΈι”™" (Today's weather is not bad / nice)
+        // Bytes:
+        //   今: BD F1
+        //   倩: CC EC
+        //   倩: CC EC
+        //   ζ°”: C6 F8
+        //   不: B2 BB
+        //   ι”™: B4 ED
+        (
+            "gbk.txt",
+            &[
+                0xbd, 0xf1, 0xcc, 0xec, 0xcc, 0xec, 0xc6, 0xf8, 0xb2, 0xbb, 0xb4, 0xed,
+            ],
+            "δ»Šε€©ε€©ζ°”δΈι”™",
+        ),
+        (
+            "utf16le_bom.txt",
+            &[
+                0xFF, 0xFE, // BOM
+                0x53, 0x30, // こ
+                0x93, 0x30, // γ‚“
+                0x6B, 0x30, // に
+                0x61, 0x30, // け
+                0x6F, 0x30, // は
+            ],
+            "こんにけは",
+        ),
+        (
+            "utf8_bom.txt",
+            &[
+                0xEF, 0xBB, 0xBF, // UTF-8 BOM
+                0xE3, 0x81, 0x93, // こ
+                0xE3, 0x82, 0x93, // γ‚“
+                0xE3, 0x81, 0xAB, // に
+                0xE3, 0x81, 0xA1, // け
+                0xE3, 0x81, 0xAF, // は
+            ],
+            "こんにけは",
+        ),
+    ];
+
+    let root_path = if cfg!(windows) {
+        Path::new("C:\\root")
+    } else {
+        Path::new("/root")
+    };
+
+    let fs = FakeFs::new(cx.background_executor.clone());
+
+    let mut files_json = serde_json::Map::new();
+    for (name, _, _) in &test_cases {
+        files_json.insert(name.to_string(), serde_json::Value::String("".to_string()));
+    }
+
+    for (name, bytes, _) in &test_cases {
+        let path = root_path.join(name);
+        fs.write(&path, bytes).await.unwrap();
+    }
+
+    let tree = Worktree::local(
+        root_path,
+        true,
+        fs,
+        Default::default(),
+        true,
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+
+    for (name, _, expected) in test_cases {
+        let loaded = tree
+            .update(cx, |tree, cx| tree.load_file(rel_path(name), cx))
+            .await
+            .with_context(|| format!("Failed to load {}", name))
+            .unwrap();
+
+        assert_eq!(
+            loaded.text, expected,
+            "Encoding mismatch for file: {}",
+            name
+        );
+    }
+}
+
+#[gpui::test]
+async fn test_write_file_encoding(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+    let fs = FakeFs::new(cx.executor());
+    let root_path = if cfg!(windows) {
+        Path::new("C:\\root")
+    } else {
+        Path::new("/root")
+    };
+    fs.create_dir(root_path).await.unwrap();
+    let file_path = root_path.join("test.txt");
+
+    fs.insert_file(&file_path, "initial".into()).await;
+
+    let worktree = Worktree::local(
+        root_path,
+        true,
+        fs.clone(),
+        Default::default(),
+        true,
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+
+    let path: Arc<Path> = Path::new("test.txt").into();
+    let rel_path = RelPath::new(&path, PathStyle::local()).unwrap().into_arc();
+
+    let text = text::Rope::from("こんにけは");
+
+    let task = worktree.update(cx, |wt, cx| {
+        wt.write_file(
+            rel_path,
+            text,
+            text::LineEnding::Unix,
+            encoding_rs::SHIFT_JIS,
+            false,
+            cx,
+        )
+    });
+
+    task.await.unwrap();
+
+    let bytes = fs.load_bytes(&file_path).await.unwrap();
+
+    let expected_bytes = vec![
+        0x82, 0xb1, // こ
+        0x82, 0xf1, // γ‚“
+        0x82, 0xc9, // に
+        0x82, 0xbf, // け
+        0x82, 0xcd, // は
+    ];
+
+    assert_eq!(bytes, expected_bytes, "Should be saved as Shift-JIS");
+}

crates/zed/Cargo.toml πŸ”—

@@ -163,6 +163,7 @@ vim_mode_setting.workspace = true
 watch.workspace = true
 web_search.workspace = true
 web_search_providers.workspace = true
+which_key.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 zed_env_vars.workspace = true
@@ -195,6 +196,10 @@ terminal_view = { workspace = true, features = ["test-support"] }
 tree-sitter-md.workspace = true
 tree-sitter-rust.workspace = true
 workspace = { workspace = true, features = ["test-support"] }
+agent_ui = { workspace = true, features = ["test-support"] }
+agent_ui_v2 = { workspace = true, features = ["test-support"] }
+search = { workspace = true, features = ["test-support"] }
+
 
 [package.metadata.bundle-dev]
 icon = ["resources/app-icon-dev@2x.png", "resources/app-icon-dev.png"]

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

@@ -36,7 +36,6 @@ use std::{
     env,
     io::{self, IsTerminal},
     path::{Path, PathBuf},
-    pin::Pin,
     process,
     sync::{Arc, OnceLock},
     time::Instant,
@@ -407,7 +406,14 @@ pub fn main() {
     });
 
     app.run(move |cx| {
-        trusted_worktrees::init(None, None, cx);
+        let trusted_paths = match workspace::WORKSPACE_DB.fetch_trusted_worktrees(None, None, cx) {
+            Ok(trusted_paths) => trusted_paths,
+            Err(e) => {
+                log::error!("Failed to do initial trusted worktrees fetch: {e:#}");
+                HashMap::default()
+            }
+        };
+        trusted_worktrees::init(trusted_paths, None, None, cx);
         menu::init();
         zed_actions::init();
 
@@ -477,14 +483,7 @@ pub fn main() {
         })
         .detach();
 
-        let trust_task = trusted_worktrees::wait_for_default_workspace_trust("Node runtime", cx)
-            .map(|trust_task| Box::pin(trust_task) as Pin<Box<_>>);
-        let node_runtime = NodeRuntime::new(
-            client.http_client(),
-            Some(shell_env_loaded_rx),
-            rx,
-            trust_task,
-        );
+        let node_runtime = NodeRuntime::new(client.http_client(), Some(shell_env_loaded_rx), rx);
 
         debug_adapter_extension::init(extension_host_proxy.clone(), cx);
         languages::init(languages.clone(), fs.clone(), node_runtime.clone(), cx);
@@ -657,6 +656,7 @@ pub fn main() {
         inspector_ui::init(app_state.clone(), cx);
         json_schema_store::init(cx);
         miniprofiler_ui::init(*STARTUP_TIME.get().unwrap(), cx);
+        which_key::init(cx);
 
         cx.observe_global::<SettingsStore>({
             let http = app_state.client.http_client();
@@ -812,7 +812,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
                         workspace::get_any_active_workspace(app_state, cx.clone()).await?;
                     workspace.update(cx, |workspace, window, cx| {
                         if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                            panel.focus_handle(cx).focus(window);
+                            panel.focus_handle(cx).focus(window, cx);
                         }
                     })
                 })
@@ -893,6 +893,44 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
                 })
                 .detach_and_log_err(cx);
             }
+            OpenRequestKind::GitCommit { sha } => {
+                cx.spawn(async move |cx| {
+                    let paths_with_position =
+                        derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await;
+                    let (workspace, _results) = open_paths_with_positions(
+                        &paths_with_position,
+                        &[],
+                        app_state,
+                        workspace::OpenOptions::default(),
+                        cx,
+                    )
+                    .await?;
+
+                    workspace
+                        .update(cx, |workspace, window, cx| {
+                            let Some(repo) = workspace.project().read(cx).active_repository(cx)
+                            else {
+                                log::error!("no active repository found for commit view");
+                                return Err(anyhow::anyhow!("no active repository found"));
+                            };
+
+                            git_ui::commit_view::CommitView::open(
+                                sha,
+                                repo.downgrade(),
+                                workspace.weak_handle(),
+                                None,
+                                None,
+                                window,
+                                cx,
+                            );
+                            Ok(())
+                        })
+                        .log_err();
+
+                    anyhow::Ok(())
+                })
+                .detach_and_log_err(cx);
+            }
         }
 
         return;

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

@@ -353,6 +353,8 @@ pub fn initialize_workspace(
 ) {
     let mut _on_close_subscription = bind_on_window_closed(cx);
     cx.observe_global::<SettingsStore>(move |cx| {
+        // A 1.92 regression causes unused-assignment to trigger on this variable.
+        _ = _on_close_subscription.is_some();
         _on_close_subscription = bind_on_window_closed(cx);
     })
     .detach();
@@ -475,7 +477,7 @@ pub fn initialize_workspace(
         initialize_panels(prompt_builder.clone(), window, cx);
         register_actions(app_state.clone(), workspace, window, cx);
 
-        workspace.focus_handle(cx).focus(window);
+        workspace.focus_handle(cx).focus(window, cx);
     })
     .detach();
 }
@@ -705,7 +707,6 @@ fn setup_or_teardown_ai_panel<P: Panel>(
         .disable_ai
         || cfg!(test);
     let existing_panel = workspace.panel::<P>(cx);
-
     match (disable_ai, existing_panel) {
         (false, None) => cx.spawn_in(window, async move |workspace, cx| {
             let panel = load_panel(workspace.clone(), cx.clone()).await?;
@@ -1109,7 +1110,21 @@ fn register_actions(
                         cx,
                         |workspace, window, cx| {
                             cx.activate(true);
-                            Editor::new_file(workspace, &Default::default(), window, cx)
+                            // Create buffer synchronously to avoid flicker
+                            let project = workspace.project().clone();
+                            let buffer = project.update(cx, |project, cx| {
+                                project.create_local_buffer("", None, true, cx)
+                            });
+                            let editor = cx.new(|cx| {
+                                Editor::for_buffer(buffer, Some(project), window, cx)
+                            });
+                            workspace.add_item_to_active_pane(
+                                Box::new(editor),
+                                None,
+                                true,
+                                window,
+                                cx,
+                            );
                         },
                     )
                     .detach();
@@ -2311,7 +2326,7 @@ mod tests {
     use project::{Project, ProjectPath};
     use semver::Version;
     use serde_json::json;
-    use settings::{SettingsStore, watch_config_file};
+    use settings::{SaturatingBool, SettingsStore, watch_config_file};
     use std::{
         path::{Path, PathBuf},
         time::Duration,
@@ -5155,6 +5170,28 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_disable_ai_crash(cx: &mut gpui::TestAppContext) {
+        let app_state = init_test(cx);
+        cx.update(init);
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
+
+        cx.run_until_parked();
+
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |settings_store, cx| {
+                settings_store.update_user_settings(cx, |settings| {
+                    settings.disable_ai = Some(SaturatingBool(true));
+                });
+            });
+        });
+
+        cx.run_until_parked();
+
+        // If this panics, the test has failed
+    }
+
     #[gpui::test]
     async fn test_prefer_focused_window(cx: &mut gpui::TestAppContext) {
         let app_state = init_test(cx);

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

@@ -161,7 +161,7 @@ impl ComponentPreview {
         component_preview.update_component_list(cx);
 
         let focus_handle = component_preview.filter_editor.read(cx).focus_handle(cx);
-        window.focus(&focus_handle);
+        window.focus(&focus_handle, cx);
 
         Ok(component_preview)
     }
@@ -770,7 +770,7 @@ impl Item for ComponentPreview {
         self.workspace_id = workspace.database_id();
 
         let focus_handle = self.filter_editor.read(cx).focus_handle(cx);
-        window.focus(&focus_handle);
+        window.focus(&focus_handle, cx);
     }
 }
 

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

@@ -145,23 +145,6 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context<Ed
             },
         ))
         .detach();
-    editor
-        .register_action(cx.listener(
-            |editor, _: &copilot::NextSuggestion, window: &mut Window, cx: &mut Context<Editor>| {
-                editor.next_edit_prediction(&Default::default(), window, cx);
-            },
-        ))
-        .detach();
-    editor
-        .register_action(cx.listener(
-            |editor,
-             _: &copilot::PreviousSuggestion,
-             window: &mut Window,
-             cx: &mut Context<Editor>| {
-                editor.previous_edit_prediction(&Default::default(), window, cx);
-            },
-        ))
-        .detach();
 }
 
 fn assign_edit_prediction_provider(

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

@@ -58,6 +58,9 @@ pub enum OpenRequestKind {
         /// `None` opens settings without navigating to a specific path.
         setting_path: Option<String>,
     },
+    GitCommit {
+        sha: String,
+    },
 }
 
 impl OpenRequest {
@@ -110,6 +113,8 @@ impl OpenRequest {
                 this.kind = Some(OpenRequestKind::Setting {
                     setting_path: Some(setting_path.to_string()),
                 });
+            } else if let Some(commit_path) = url.strip_prefix("zed://git/commit/") {
+                this.parse_git_commit_url(commit_path)?
             } else if url.starts_with("ssh://") {
                 this.parse_ssh_file_path(&url, cx)?
             } else if let Some(zed_link) = parse_zed_link(&url, cx) {
@@ -138,6 +143,28 @@ impl OpenRequest {
         }
     }
 
+    fn parse_git_commit_url(&mut self, commit_path: &str) -> Result<()> {
+        // Format: <sha>?repo=<path>
+        let (sha, query) = commit_path
+            .split_once('?')
+            .context("invalid git commit url: missing query string")?;
+        anyhow::ensure!(!sha.is_empty(), "invalid git commit url: missing sha");
+
+        let repo = url::form_urlencoded::parse(query.as_bytes())
+            .find_map(|(key, value)| (key == "repo").then_some(value))
+            .filter(|s| !s.is_empty())
+            .context("invalid git commit url: missing repo query parameter")?
+            .to_string();
+
+        self.open_paths.push(repo);
+
+        self.kind = Some(OpenRequestKind::GitCommit {
+            sha: sha.to_string(),
+        });
+
+        Ok(())
+    }
+
     fn parse_ssh_file_path(&mut self, file: &str, cx: &App) -> Result<()> {
         let url = url::Url::parse(file)?;
         let host = url
@@ -688,6 +715,86 @@ mod tests {
         assert_eq!(request.open_paths, vec!["/"]);
     }
 
+    #[gpui::test]
+    fn test_parse_git_commit_url(cx: &mut TestAppContext) {
+        let _app_state = init_test(cx);
+
+        // Test basic git commit URL
+        let request = cx.update(|cx| {
+            OpenRequest::parse(
+                RawOpenRequest {
+                    urls: vec!["zed://git/commit/abc123?repo=path/to/repo".into()],
+                    ..Default::default()
+                },
+                cx,
+            )
+            .unwrap()
+        });
+
+        match request.kind.unwrap() {
+            OpenRequestKind::GitCommit { sha } => {
+                assert_eq!(sha, "abc123");
+            }
+            _ => panic!("expected GitCommit variant"),
+        }
+        // Verify path was added to open_paths for workspace routing
+        assert_eq!(request.open_paths, vec!["path/to/repo"]);
+
+        // Test with URL encoded path
+        let request = cx.update(|cx| {
+            OpenRequest::parse(
+                RawOpenRequest {
+                    urls: vec!["zed://git/commit/def456?repo=path%20with%20spaces".into()],
+                    ..Default::default()
+                },
+                cx,
+            )
+            .unwrap()
+        });
+
+        match request.kind.unwrap() {
+            OpenRequestKind::GitCommit { sha } => {
+                assert_eq!(sha, "def456");
+            }
+            _ => panic!("expected GitCommit variant"),
+        }
+        assert_eq!(request.open_paths, vec!["path with spaces"]);
+
+        // Test with empty path
+        cx.update(|cx| {
+            assert!(
+                OpenRequest::parse(
+                    RawOpenRequest {
+                        urls: vec!["zed://git/commit/abc123?repo=".into()],
+                        ..Default::default()
+                    },
+                    cx,
+                )
+                .unwrap_err()
+                .to_string()
+                .contains("missing repo")
+            );
+        });
+
+        // Test error case: missing SHA
+        let result = cx.update(|cx| {
+            OpenRequest::parse(
+                RawOpenRequest {
+                    urls: vec!["zed://git/commit/abc123?foo=bar".into()],
+                    ..Default::default()
+                },
+                cx,
+            )
+        });
+        assert!(result.is_err());
+        assert!(
+            result
+                .unwrap_err()
+                .to_string()
+                .contains("missing repo query parameter")
+        );
+    }
+
     #[gpui::test]
     async fn test_open_workspace_with_directory(cx: &mut TestAppContext) {
         let app_state = init_test(cx);

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

@@ -354,6 +354,8 @@ pub mod agent {
             ResetAgentZoom,
             /// Toggles the utility/agent pane open/closed state.
             ToggleAgentPane,
+            /// Pastes clipboard content without any formatting.
+            PasteRaw,
         ]
     );
 }

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

@@ -1,8 +1,8 @@
-pub use tracing::Level;
+pub use tracing::{Level, field};
 
 #[cfg(ztracing)]
 pub use tracing::{
-    debug_span, error_span, event, info_span, instrument, span, trace_span, warn_span,
+    Span, debug_span, error_span, event, info_span, instrument, span, trace_span, warn_span,
 };
 #[cfg(not(ztracing))]
 pub use ztracing_macro::instrument;
@@ -26,17 +26,23 @@ pub use __consume_all_tokens as span;
 #[macro_export]
 macro_rules! __consume_all_tokens {
     ($($t:tt)*) => {
-        $crate::FakeSpan
+        $crate::Span
     };
 }
 
-pub struct FakeSpan;
-impl FakeSpan {
+#[cfg(not(ztracing))]
+pub struct Span;
+
+#[cfg(not(ztracing))]
+impl Span {
+    pub fn current() -> Self {
+        Self
+    }
+
     pub fn enter(&self) {}
-}
 
-// #[cfg(not(ztracing))]
-// pub use span;
+    pub fn record<T, S>(&self, _t: T, _s: S) {}
+}
 
 #[cfg(ztracing)]
 pub fn init() {

docs/src/completions.md πŸ”—

@@ -15,6 +15,10 @@ When there is an appropriate language server available, Zed will provide complet
 
 You can manually trigger completions with `ctrl-space` or by triggering the `editor::ShowCompletions` action from the command palette.
 
+> Note: Using `ctrl-space` in Zed requires disabling the macOS global shortcut.
+> Open **System Settings** > **Keyboard** > **Keyboard Shortcut**s >
+> **Input Sources** and uncheck **Select the previous input source**.
+
 For more information, see:
 
 - [Configuring Supported Languages](./configuring-languages.md)

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

@@ -73,7 +73,7 @@ h_flex()
 
 - `Window`: A struct in zed representing a zed window in your desktop environment (see image below). There can be multiple if you have multiple zed instances open. Mostly passed around for rendering.
 - `Modal`: A UI element that floats on top of the rest of the UI
-- `Picker`: A struct representing a list of items in floating on top of the UI (Modal). You can select an item and confirm. What happens on select or confirm is determined by the picker's delegate. (The 'Model' in the image below is a picker.)
+- `Picker`: A struct representing a list of items floating on top of the UI (Modal). You can select an item and confirm. What happens on select or confirm is determined by the picker's delegate. (The 'Modal' in the image below is a picker.)
 - `PickerDelegate`: A trait used to specialize behavior for a `Picker`. The `Picker` stores the `PickerDelegate` in the field delegate.
 - `Center`: The middle of the zed window, the center is split into multiple `Pane`s. In the codebase this is a field on the `Workspace` struct. (see image below).
 - `Pane`: An area in the `Center` where we can place items, such as an editor, multi-buffer or terminal (see image below).

docs/src/migrate/_research-notes.md πŸ”—

@@ -0,0 +1,73 @@
+<!--
+  TEMPORARY RESEARCH FILE - Delete when migration guides are complete
+
+  This file contains external community insights used to add "flair" to migration guides.
+  These are NOT the template or backboneβ€”use intellij.md as the structural template.
+
+  STATUS:
+  βœ… PyCharm guide - COMPLETE
+  βœ… WebStorm guide - COMPLETE
+  βœ… RustRover guide - COMPLETE
+-->
+
+# Migration Research Notes
+
+## Completed Guides
+
+All three JetBrains migration guides have been populated with full content:
+
+1. **pycharm.md** - Python development, virtual environments, Ruff/Pyright, Django/Flask workflows
+2. **webstorm.md** - JavaScript/TypeScript development, npm workflows, framework considerations
+3. **rustrover.md** - Rust development, rust-analyzer parity, Cargo workflows, licensing notes
+
+## Key Sources Used
+
+- IntelliJ IDEA migration doc (structural template)
+- JetBrains PyCharm Getting Started docs
+- JetBrains WebStorm Getting Started docs
+- JetBrains RustRover Quick Start Guide
+- External community feedback (Reddit, Hacker News, Medium)
+
+## External Quotes Incorporated
+
+### WebStorm Guide
+
+> "I work for AWS and the applications I deal with are massive. Often I need to keep many projects open due to tight dependencies. I'm talking about complex microservices and micro frontend infrastructure which oftentimes lead to 2-15 minutes of indexing wait time whenever I open a project or build the system locally."
+
+### RustRover Guide
+
+- Noted rust-analyzer shared foundation between RustRover and Zed
+- Addressed licensing/telemetry concerns that motivate some users to switch
+- Included debugger caveats based on community feedback
+
+## Cross-Cutting Themes Applied to All Guides
+
+### Universal Pain Points Addressed
+
+1. Indexing (instant in Zed)
+2. Resource usage (Zed is lightweight)
+3. Startup time (Zed is near-instant)
+4. UI clutter (Zed is minimal by design)
+
+### Universal Missing Features Documented
+
+- No project model / SDK management
+- No database tools
+- No framework-specific integration
+- No visual run configurations (use tasks)
+- No built-in HTTP client
+
+### JetBrains Keymap Emphasized
+
+All three guides emphasize:
+
+- Select JetBrains keymap during onboarding or in settings
+- `Shift Shift` for Search Everywhere works
+- Most familiar shortcuts preserved
+
+## Next Steps (Optional Enhancements)
+
+- [ ] Cross-link guides to JetBrains docs for users who want to reference original IDE features
+- [ ] Add a consolidated "hub page" linking to all migration guides
+- [ ] Consider adding VS Code migration guide using similar structure
+- [ ] Review for tone consistency against Zed Documentation Guidelines

flake.lock πŸ”—

@@ -2,11 +2,11 @@
   "nodes": {
     "crane": {
       "locked": {
-        "lastModified": 1762538466,
-        "narHash": "sha256-8zrIPl6J+wLm9MH5ksHcW7BUHo7jSNOu0/hA0ohOOaM=",
+        "lastModified": 1765145449,
+        "narHash": "sha256-aBVHGWWRzSpfL++LubA0CwOOQ64WNLegrYHwsVuVN7A=",
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "0cea393fffb39575c46b7a0318386467272182fe",
+        "rev": "69f538cdce5955fcd47abfed4395dc6d5194c1c5",
         "type": "github"
       },
       "original": {
@@ -17,11 +17,11 @@
     },
     "flake-compat": {
       "locked": {
-        "lastModified": 1761588595,
-        "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
+        "lastModified": 1765121682,
+        "narHash": "sha256-4VBOP18BFeiPkyhy9o4ssBNQEvfvv1kXkasAYd0+rrA=",
         "owner": "edolstra",
         "repo": "flake-compat",
-        "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
+        "rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
         "type": "github"
       },
       "original": {
@@ -32,11 +32,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 315532800,
-        "narHash": "sha256-5CwQ80ucRHiqVbMEEbTFnjz70/axSJ0aliyzSaFSkmY=",
-        "rev": "f6b44b2401525650256b977063dbcf830f762369",
+        "lastModified": 1765772535,
+        "narHash": "sha256-I715zWsdVZ+CipmLtoCAeNG0etQywiWRE5PaWntnaYk=",
+        "rev": "09b8fda8959d761445f12b55f380d90375a1d6bb",
         "type": "tarball",
-        "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre891648.f6b44b240152/nixexprs.tar.xz"
+        "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre911985.09b8fda8959d/nixexprs.tar.xz"
       },
       "original": {
         "type": "tarball",
@@ -53,16 +53,14 @@
     },
     "rust-overlay": {
       "inputs": {
-        "nixpkgs": [
-          "nixpkgs"
-        ]
+        "nixpkgs": ["nixpkgs"]
       },
       "locked": {
-        "lastModified": 1762915112,
-        "narHash": "sha256-d9j1g8nKmYDHy+/bIOPQTh9IwjRliqaTM0QLHMV92Ic=",
+        "lastModified": 1765465581,
+        "narHash": "sha256-fCXT0aZXmTalM3NPCTedVs9xb0egBG5BOZkcrYo5PGE=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "aa1e85921cfa04de7b6914982a94621fbec5cc02",
+        "rev": "99cc5667eece98bb35dcf35f7e511031a8b7a125",
         "type": "github"
       },
       "original": {

flake.nix πŸ”—

@@ -37,14 +37,14 @@
           rustToolchain = rustBin.fromRustupToolchainFile ./rust-toolchain.toml;
         };
     in
-    rec {
+    {
       packages = forAllSystems (pkgs: rec {
         default = mkZed pkgs;
         debug = default.override { profile = "dev"; };
       });
       devShells = forAllSystems (pkgs: {
         default = pkgs.callPackage ./nix/shell.nix {
-          zed-editor = packages.${pkgs.hostPlatform.system}.default;
+          zed-editor = mkZed pkgs;
         };
       });
       formatter = forAllSystems (pkgs: pkgs.nixfmt-rfc-style);

nix/build.nix πŸ”—

@@ -83,70 +83,94 @@ let
 
       cargoLock = ../Cargo.lock;
 
-      nativeBuildInputs =
-        [
-          cmake
-          copyDesktopItems
-          curl
-          perl
-          pkg-config
-          protobuf
-          cargo-about
-          rustPlatform.bindgenHook
-        ]
-        ++ lib.optionals stdenv'.hostPlatform.isLinux [ makeWrapper ]
-        ++ lib.optionals stdenv'.hostPlatform.isDarwin [
-          (cargo-bundle.overrideAttrs (
-            new: old: {
-              version = "0.6.1-zed";
-              src = fetchFromGitHub {
-                owner = "zed-industries";
-                repo = "cargo-bundle";
-                rev = "2be2669972dff3ddd4daf89a2cb29d2d06cad7c7";
-                hash = "sha256-cSvW0ND148AGdIGWg/ku0yIacVgW+9f1Nsi+kAQxVrI=";
-              };
-              cargoHash = "sha256-urn+A3yuw2uAO4HGmvQnKvWtHqvG9KHxNCCWTiytE4k=";
-
-              # NOTE: can drop once upstream uses `finalAttrs` here:
-              # https://github.com/NixOS/nixpkgs/blob/10214747f5e6e7cb5b9bdf9e018a3c7b3032f5af/pkgs/build-support/rust/build-rust-package/default.nix#L104
-              #
-              # See (for context): https://github.com/NixOS/nixpkgs/pull/382550
-              cargoDeps = rustPlatform.fetchCargoVendor {
-                inherit (new) src;
-                hash = new.cargoHash;
-                patches = new.cargoPatches or [];
-                name = new.cargoDepsName or new.finalPackage.name;
-              };
-            }
-          ))
-        ];
-
-      buildInputs =
-        [
-          curl
-          fontconfig
-          freetype
-          # TODO: need staticlib of this for linking the musl remote server.
-          # should make it a separate derivation/flake output
-          # see https://crane.dev/examples/cross-musl.html
-          libgit2
-          openssl
-          sqlite
-          zlib
-          zstd
-        ]
-        ++ lib.optionals stdenv'.hostPlatform.isLinux [
-          alsa-lib
-          libxkbcommon
-          wayland
-          gpu-lib
-          xorg.libX11
-          xorg.libxcb
-        ]
-        ++ lib.optionals stdenv'.hostPlatform.isDarwin [
-          apple-sdk_15
-          (darwinMinVersionHook "10.15")
-        ];
+      nativeBuildInputs = [
+        cmake
+        copyDesktopItems
+        curl
+        perl
+        pkg-config
+        protobuf
+        # Pin cargo-about to 0.8.2. Newer versions don't work with the current license identifiers
+        # See https://github.com/zed-industries/zed/pull/44012
+        (cargo-about.overrideAttrs (
+          new: old: rec {
+            version = "0.8.2";
+
+            src = fetchFromGitHub {
+              owner = "EmbarkStudios";
+              repo = "cargo-about";
+              tag = version;
+              sha256 = "sha256-cNKZpDlfqEXeOE5lmu79AcKOawkPpk4PQCsBzNtIEbs=";
+            };
+
+            cargoHash = "sha256-NnocSs6UkuF/mCM3lIdFk+r51Iz2bHuYzMT/gEbT/nk=";
+
+            # NOTE: can drop once upstream uses `finalAttrs` here:
+            # https://github.com/NixOS/nixpkgs/blob/10214747f5e6e7cb5b9bdf9e018a3c7b3032f5af/pkgs/build-support/rust/build-rust-package/default.nix#L104
+            #
+            # See (for context): https://github.com/NixOS/nixpkgs/pull/382550
+            cargoDeps = rustPlatform.fetchCargoVendor {
+              inherit (new) src;
+              hash = new.cargoHash;
+              patches = new.cargoPatches or [ ];
+              name = new.cargoDepsName or new.finalPackage.name;
+            };
+          }
+        ))
+        rustPlatform.bindgenHook
+      ]
+      ++ lib.optionals stdenv'.hostPlatform.isLinux [ makeWrapper ]
+      ++ lib.optionals stdenv'.hostPlatform.isDarwin [
+        (cargo-bundle.overrideAttrs (
+          new: old: {
+            version = "0.6.1-zed";
+            src = fetchFromGitHub {
+              owner = "zed-industries";
+              repo = "cargo-bundle";
+              rev = "2be2669972dff3ddd4daf89a2cb29d2d06cad7c7";
+              hash = "sha256-cSvW0ND148AGdIGWg/ku0yIacVgW+9f1Nsi+kAQxVrI=";
+            };
+            cargoHash = "sha256-urn+A3yuw2uAO4HGmvQnKvWtHqvG9KHxNCCWTiytE4k=";
+
+            # NOTE: can drop once upstream uses `finalAttrs` here:
+            # https://github.com/NixOS/nixpkgs/blob/10214747f5e6e7cb5b9bdf9e018a3c7b3032f5af/pkgs/build-support/rust/build-rust-package/default.nix#L104
+            #
+            # See (for context): https://github.com/NixOS/nixpkgs/pull/382550
+            cargoDeps = rustPlatform.fetchCargoVendor {
+              inherit (new) src;
+              hash = new.cargoHash;
+              patches = new.cargoPatches or [ ];
+              name = new.cargoDepsName or new.finalPackage.name;
+            };
+          }
+        ))
+      ];
+
+      buildInputs = [
+        curl
+        fontconfig
+        freetype
+        # TODO: need staticlib of this for linking the musl remote server.
+        # should make it a separate derivation/flake output
+        # see https://crane.dev/examples/cross-musl.html
+        libgit2
+        openssl
+        sqlite
+        zlib
+        zstd
+      ]
+      ++ lib.optionals stdenv'.hostPlatform.isLinux [
+        alsa-lib
+        libxkbcommon
+        wayland
+        gpu-lib
+        xorg.libX11
+        xorg.libxcb
+      ]
+      ++ lib.optionals stdenv'.hostPlatform.isDarwin [
+        apple-sdk_15
+        (darwinMinVersionHook "10.15")
+      ];
 
       cargoExtraArgs = "-p zed -p cli --locked --features=gpui/runtime_shaders";
 
@@ -177,7 +201,7 @@ let
         ZED_UPDATE_EXPLANATION = "Zed has been installed using Nix. Auto-updates have thus been disabled.";
         RELEASE_VERSION = version;
         LK_CUSTOM_WEBRTC = livekit-libwebrtc;
-        PROTOC="${protobuf}/bin/protoc";
+        PROTOC = "${protobuf}/bin/protoc";
 
         CARGO_PROFILE = profile;
         # need to handle some profiles specially https://github.com/rust-lang/cargo/issues/11053
@@ -217,14 +241,13 @@ let
             # `webrtc-sys` expects a staticlib; nixpkgs' `livekit-webrtc` has been patched to
             # produce a `dylib`... patching `webrtc-sys`'s build script is the easier option
             # TODO: send livekit sdk a PR to make this configurable
-            postPatch =
-              ''
-                substituteInPlace webrtc-sys/build.rs --replace-fail \
-                  "cargo:rustc-link-lib=static=webrtc" "cargo:rustc-link-lib=dylib=webrtc"
-              ''
-              + lib.optionalString withGLES ''
-                cat ${glesConfig} >> .cargo/config/config.toml
-              '';
+            postPatch = ''
+              substituteInPlace webrtc-sys/build.rs --replace-fail \
+                "cargo:rustc-link-lib=static=webrtc" "cargo:rustc-link-lib=dylib=webrtc"
+            ''
+            + lib.optionalString withGLES ''
+              cat ${glesConfig} >> .cargo/config/config.toml
+            '';
           in
           crates: drv:
           if hasWebRtcSys crates then

rust-toolchain.toml πŸ”—

@@ -1,5 +1,5 @@
 [toolchain]
-channel = "1.91.1"
+channel = "1.92"
 profile = "minimal"
 components = [ "rustfmt", "clippy" ]
 targets = [

script/bundle-mac πŸ”—

@@ -106,6 +106,17 @@ mv Cargo.toml.backup Cargo.toml
 popd
 echo "Bundled ${app_path}"
 
+# DocumentTypes.plist references CFBundleTypeIconFile "Document", so the bundle must contain Document.icns.
+# We use the app icon as a placeholder document icon for now.
+document_icon_source="crates/zed/resources/Document.icns"
+document_icon_target="${app_path}/Contents/Resources/Document.icns"
+if [[ -f "${document_icon_source}" ]]; then
+    mkdir -p "$(dirname "${document_icon_target}")"
+    cp "${document_icon_source}" "${document_icon_target}"
+else
+    echo "cargo::warning=Missing ${document_icon_source}; macOS document icons may not appear in Finder."
+fi
+
 if [[ -n "${MACOS_CERTIFICATE:-}" && -n "${MACOS_CERTIFICATE_PASSWORD:-}" && -n "${APPLE_NOTARIZATION_KEY:-}" && -n "${APPLE_NOTARIZATION_KEY_ID:-}" && -n "${APPLE_NOTARIZATION_ISSUER_ID:-}" ]]; then
     can_code_sign=true
 

script/danger/dangerfile.ts πŸ”—

@@ -6,6 +6,9 @@ prHygiene({
   rules: {
     // Don't enable this rule just yet, as it can have false positives.
     useImperativeMood: "off",
+    noConventionalCommits: {
+      bannedTypes: ["feat", "fix", "style", "refactor", "perf", "test", "chore", "build", "revert"],
+    },
   },
 });
 

script/danger/package.json πŸ”—

@@ -8,6 +8,6 @@
   },
   "devDependencies": {
     "danger": "13.0.4",
-    "danger-plugin-pr-hygiene": "0.6.1"
+    "danger-plugin-pr-hygiene": "0.7.0"
   }
 }

script/danger/pnpm-lock.yaml πŸ”—

@@ -12,8 +12,8 @@ importers:
         specifier: 13.0.4
         version: 13.0.4
       danger-plugin-pr-hygiene:
-        specifier: 0.6.1
-        version: 0.6.1
+        specifier: 0.7.0
+        version: 0.7.0
 
 packages:
 
@@ -134,8 +134,8 @@ packages:
   core-js@3.45.1:
     resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==}
 
-  danger-plugin-pr-hygiene@0.6.1:
-    resolution: {integrity: sha512-nb+iUQvirE3BlKXI1WoOND6sujyGzHar590mJm5tt4RLi65HXFaU5hqONxgDoWFujJNHYnXse9yaZdxnxEi4QA==}
+  danger-plugin-pr-hygiene@0.7.0:
+    resolution: {integrity: sha512-YDWhEodP0fg/t9YO3SxufWS9j1Rcxbig+1flTlUlojBDFiKQyVmaj8PIvnJxJItjHWTlNKI9wMSRq5vUql6zyA==}
 
   danger@13.0.4:
     resolution: {integrity: sha512-IAdQ5nSJyIs4zKj6AN35ixt2B0Ce3WZUm3IFe/CMnL/Op7wV7IGg4D348U0EKNaNPP58QgXbdSk9pM+IXP1QXg==}
@@ -573,7 +573,7 @@ snapshots:
 
   core-js@3.45.1: {}
 
-  danger-plugin-pr-hygiene@0.6.1: {}
+  danger-plugin-pr-hygiene@0.7.0: {}
 
   danger@13.0.4:
     dependencies:

script/verify-macos-document-icon πŸ”—

@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+usage() {
+  cat <<'USAGE'
+Usage:
+  script/verify-macos-document-icon /path/to/Zed.app
+
+Verifies that the given macOS app bundle's Info.plist references a document icon
+named "Document" and that the corresponding icon file exists in the bundle.
+
+Specifically checks:
+  - CFBundleDocumentTypes[*].CFBundleTypeIconFile includes "Document"
+  - Contents/Resources/Document.icns exists
+
+Exit codes:
+  0 - success
+  1 - verification failed
+  2 - invalid usage / missing prerequisites
+USAGE
+}
+
+fail() {
+  echo "error: $*" >&2
+  exit 1
+}
+
+if [[ $# -ne 1 ]]; then
+  usage >&2
+  exit 2
+fi
+
+app_path="$1"
+
+if [[ ! -d "${app_path}" ]]; then
+  fail "app bundle not found: ${app_path}"
+fi
+
+info_plist="${app_path}/Contents/Info.plist"
+if [[ ! -f "${info_plist}" ]]; then
+  fail "missing Info.plist: ${info_plist}"
+fi
+
+if ! command -v plutil >/dev/null 2>&1; then
+  fail "plutil not found (required on macOS to read Info.plist)"
+fi
+
+# Convert to JSON for robust parsing. plutil outputs JSON to stdout in this mode.
+info_json="$(plutil -convert json -o - "${info_plist}")"
+
+# Check that CFBundleDocumentTypes exists and that at least one entry references "Document".
+# We use Python for JSON parsing; macOS ships with Python 3 on many setups, but not all.
+# If python3 isn't available, fall back to a simpler grep-based check.
+has_document_icon_ref="false"
+if command -v python3 >/dev/null 2>&1; then
+  has_document_icon_ref="$(python3 -c "import json,sys; d=json.load(sys.stdin); types=d.get('CFBundleDocumentTypes', []); vals=[t.get('CFBundleTypeIconFile') for t in types if isinstance(t, dict)]; print('true' if 'Document' in vals else 'false')" <<<"${info_json}")"
+else
+  # This is a best-effort fallback. It may produce false negatives if the JSON formatting differs.
+  if echo "${info_json}" | grep -q '"CFBundleTypeIconFile"[[:space:]]*:[[:space:]]*"Document"'; then
+    has_document_icon_ref="true"
+  fi
+fi
+
+if [[ "${has_document_icon_ref}" != "true" ]]; then
+  echo "Verification failed for: ${app_path}" >&2
+  echo "Expected Info.plist to reference CFBundleTypeIconFile \"Document\" in CFBundleDocumentTypes." >&2
+  echo "Tip: This bundle may be missing DocumentTypes.plist extensions or may have different icon naming." >&2
+  exit 1
+fi
+
+document_icon_path="${app_path}/Contents/Resources/Document.icns"
+if [[ ! -f "${document_icon_path}" ]]; then
+  echo "Verification failed for: ${app_path}" >&2
+  echo "Expected document icon to exist: ${document_icon_path}" >&2
+  echo "Tip: The bundle script should copy crates/zed/resources/Document.icns into Contents/Resources/Document.icns." >&2
+  exit 1
+fi
+
+echo "OK: ${app_path}"
+echo " - Info.plist references CFBundleTypeIconFile \"Document\""
+echo " - Found ${document_icon_path}"

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

@@ -8,31 +8,55 @@ use crate::tasks::workflows::{
 
 pub fn autofix_pr() -> Workflow {
     let pr_number = WorkflowInput::string("pr_number", None);
-    let autofix = run_autofix(&pr_number);
+    let run_clippy = WorkflowInput::bool("run_clippy", Some(true));
+    let run_autofix = run_autofix(&pr_number, &run_clippy);
+    let commit_changes = commit_changes(&pr_number, &run_autofix);
     named::workflow()
         .run_name(format!("autofix PR #{pr_number}"))
         .on(Event::default().workflow_dispatch(
-            WorkflowDispatch::default().add_input(pr_number.name, pr_number.input()),
+            WorkflowDispatch::default()
+                .add_input(pr_number.name, pr_number.input())
+                .add_input(run_clippy.name, run_clippy.input()),
         ))
-        .add_job(autofix.name, autofix.job)
+        .concurrency(
+            Concurrency::new(Expression::new(format!(
+                "${{{{ github.workflow }}}}-{pr_number}"
+            )))
+            .cancel_in_progress(true),
+        )
+        .add_job(run_autofix.name.clone(), run_autofix.job)
+        .add_job(commit_changes.name, commit_changes.job)
 }
 
-fn run_autofix(pr_number: &WorkflowInput) -> NamedJob {
-    fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
-        let step = named::uses(
+const PATCH_ARTIFACT_NAME: &str = "autofix-patch";
+const PATCH_FILE_PATH: &str = "autofix.patch";
+
+fn upload_patch_artifact() -> Step<Use> {
+    Step::new(format!("upload artifact {}", PATCH_ARTIFACT_NAME))
+        .uses(
             "actions",
-            "create-github-app-token",
-            "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
+            "upload-artifact",
+            "330a01c490aca151604b8cf639adc76d48f6c5d4", // v5
         )
-        .add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
-        .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
-        .id("get-app-token");
-        let output = StepOutput::new(&step, "token");
-        (step, output)
-    }
+        .add_with(("name", PATCH_ARTIFACT_NAME))
+        .add_with(("path", PATCH_FILE_PATH))
+        .add_with(("if-no-files-found", "ignore"))
+        .add_with(("retention-days", "1"))
+}
 
-    fn checkout_pr(pr_number: &WorkflowInput, token: &StepOutput) -> Step<Run> {
-        named::bash(&format!("gh pr checkout {pr_number}")).add_env(("GITHUB_TOKEN", token))
+fn download_patch_artifact() -> Step<Use> {
+    named::uses(
+        "actions",
+        "download-artifact",
+        "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53", // v6.0.0
+    )
+    .add_with(("name", PATCH_ARTIFACT_NAME))
+}
+
+fn run_autofix(pr_number: &WorkflowInput, run_clippy: &WorkflowInput) -> NamedJob {
+    fn checkout_pr(pr_number: &WorkflowInput) -> Step<Run> {
+        named::bash(&format!("gh pr checkout {pr_number}"))
+            .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN))
     }
 
     fn run_cargo_fmt() -> Step<Run> {
@@ -49,16 +73,55 @@ fn run_autofix(pr_number: &WorkflowInput) -> NamedJob {
         named::bash("./script/prettier --write")
     }
 
-    fn commit_and_push(token: &StepOutput) -> Step<Run> {
+    fn create_patch() -> Step<Run> {
         named::bash(indoc::indoc! {r#"
             if git diff --quiet; then
                 echo "No changes to commit"
+                echo "has_changes=false" >> "$GITHUB_OUTPUT"
             else
-                git add -A
-                git commit -m "Autofix"
-                git push
+                git diff > autofix.patch
+                echo "has_changes=true" >> "$GITHUB_OUTPUT"
             fi
         "#})
+        .id("create-patch")
+    }
+
+    named::job(
+        Job::default()
+            .runs_on(runners::LINUX_DEFAULT)
+            .outputs([(
+                "has_changes".to_owned(),
+                "${{ steps.create-patch.outputs.has_changes }}".to_owned(),
+            )])
+            .add_step(steps::checkout_repo())
+            .add_step(checkout_pr(pr_number))
+            .add_step(steps::setup_cargo_config(runners::Platform::Linux))
+            .add_step(steps::cache_rust_dependencies_namespace())
+            .map(steps::install_linux_dependencies)
+            .add_step(steps::setup_pnpm())
+            .add_step(run_prettier_fix())
+            .add_step(run_cargo_fmt())
+            .add_step(run_clippy_fix().if_condition(Expression::new(run_clippy.to_string())))
+            .add_step(create_patch())
+            .add_step(upload_patch_artifact())
+            .add_step(steps::cleanup_cargo_config(runners::Platform::Linux)),
+    )
+}
+
+fn commit_changes(pr_number: &WorkflowInput, autofix_job: &NamedJob) -> NamedJob {
+    fn checkout_pr(pr_number: &WorkflowInput, token: &StepOutput) -> Step<Run> {
+        named::bash(&format!("gh pr checkout {pr_number}")).add_env(("GITHUB_TOKEN", token))
+    }
+
+    fn apply_patch() -> Step<Run> {
+        named::bash("git apply autofix.patch")
+    }
+
+    fn commit_and_push(token: &StepOutput) -> Step<Run> {
+        named::bash(indoc::indoc! {r#"
+            git commit -am "Autofix"
+            git push
+        "#})
         .add_env(("GIT_COMMITTER_NAME", "Zed Zippy"))
         .add_env((
             "GIT_COMMITTER_EMAIL",
@@ -72,22 +135,21 @@ fn run_autofix(pr_number: &WorkflowInput) -> NamedJob {
         .add_env(("GITHUB_TOKEN", token))
     }
 
-    let (authenticate, token) = authenticate_as_zippy();
+    let (authenticate, token) = steps::authenticate_as_zippy();
 
     named::job(
         Job::default()
-            .runs_on(runners::LINUX_DEFAULT)
+            .runs_on(runners::LINUX_SMALL)
+            .needs(vec![autofix_job.name.clone()])
+            .cond(Expression::new(format!(
+                "needs.{}.outputs.has_changes == 'true'",
+                autofix_job.name
+            )))
             .add_step(authenticate)
             .add_step(steps::checkout_repo_with_token(&token))
             .add_step(checkout_pr(pr_number, &token))
-            .add_step(steps::setup_cargo_config(runners::Platform::Linux))
-            .add_step(steps::cache_rust_dependencies_namespace())
-            .map(steps::install_linux_dependencies)
-            .add_step(steps::setup_pnpm())
-            .add_step(run_prettier_fix())
-            .add_step(run_cargo_fmt())
-            .add_step(run_clippy_fix())
-            .add_step(commit_and_push(&token))
-            .add_step(steps::cleanup_cargo_config(runners::Platform::Linux)),
+            .add_step(download_patch_artifact())
+            .add_step(apply_patch())
+            .add_step(commit_and_push(&token)),
     )
 }

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

@@ -3,7 +3,7 @@ use gh_workflow::*;
 use crate::tasks::workflows::{
     runners,
     steps::{self, NamedJob, named},
-    vars::{self, StepOutput, WorkflowInput},
+    vars::{StepOutput, WorkflowInput},
 };
 
 pub fn cherry_pick() -> Workflow {
@@ -29,19 +29,6 @@ fn run_cherry_pick(
     commit: &WorkflowInput,
     channel: &WorkflowInput,
 ) -> NamedJob {
-    fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
-        let step = named::uses(
-            "actions",
-            "create-github-app-token",
-            "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
-        ) // v2
-        .add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
-        .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
-        .id("get-app-token");
-        let output = StepOutput::new(&step, "token");
-        (step, output)
-    }
-
     fn cherry_pick(
         branch: &WorkflowInput,
         commit: &WorkflowInput,
@@ -54,7 +41,7 @@ fn run_cherry_pick(
             .add_env(("GITHUB_TOKEN", token))
     }
 
-    let (authenticate, token) = authenticate_as_zippy();
+    let (authenticate, token) = steps::authenticate_as_zippy();
 
     named::job(
         Job::default()

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

@@ -97,17 +97,20 @@ pub(crate) fn create_sentry_release() -> Step<Use> {
 }
 
 fn auto_release_preview(deps: &[&NamedJob; 1]) -> NamedJob {
+    let (authenticate, token) = steps::authenticate_as_zippy();
+
     named::job(
         dependant_job(deps)
             .runs_on(runners::LINUX_SMALL)
             .cond(Expression::new(indoc::indoc!(
                 r#"startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')"#
             )))
+            .add_step(authenticate)
             .add_step(
                 steps::script(
                     r#"gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false"#,
                 )
-                .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)),
+                .add_env(("GITHUB_TOKEN", &token)),
             )
     )
 }

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

@@ -45,11 +45,15 @@ pub(crate) fn run_tests() -> Workflow {
         &should_run_tests,
     ]);
 
+    let check_style = check_style();
+    let run_tests_linux = run_platform_tests(Platform::Linux);
+    let call_autofix = call_autofix(&check_style, &run_tests_linux);
+
     let mut jobs = vec![
         orchestrate,
-        check_style(),
+        check_style,
         should_run_tests.guard(run_platform_tests(Platform::Windows)),
-        should_run_tests.guard(run_platform_tests(Platform::Linux)),
+        should_run_tests.guard(run_tests_linux),
         should_run_tests.guard(run_platform_tests(Platform::Mac)),
         should_run_tests.guard(doctests()),
         should_run_tests.guard(check_workspace_binaries()),
@@ -106,6 +110,7 @@ pub(crate) fn run_tests() -> Workflow {
             workflow
         })
         .add_job(tests_pass.name, tests_pass.job)
+        .add_job(call_autofix.name, call_autofix.job)
 }
 
 // Generates a bash script that checks changed files against regex patterns
@@ -221,6 +226,8 @@ pub fn tests_pass(jobs: &[NamedJob]) -> NamedJob {
     named::job(job)
 }
 
+pub const STYLE_FAILED_OUTPUT: &str = "style_failed";
+
 fn check_style() -> NamedJob {
     fn check_for_typos() -> Step<Use> {
         named::uses(
@@ -236,14 +243,58 @@ fn check_style() -> NamedJob {
             .add_step(steps::checkout_repo())
             .add_step(steps::cache_rust_dependencies_namespace())
             .add_step(steps::setup_pnpm())
-            .add_step(steps::script("./script/prettier"))
+            .add_step(steps::prettier())
+            .add_step(steps::cargo_fmt())
+            .add_step(steps::record_style_failure())
             .add_step(steps::script("./script/check-todos"))
             .add_step(steps::script("./script/check-keymaps"))
             .add_step(check_for_typos())
-            .add_step(steps::cargo_fmt()),
+            .outputs([(
+                STYLE_FAILED_OUTPUT.to_owned(),
+                format!(
+                    "${{{{ steps.{}.outputs.failed == 'true' }}}}",
+                    steps::RECORD_STYLE_FAILURE_STEP_ID
+                ),
+            )]),
     )
 }
 
+fn call_autofix(check_style: &NamedJob, run_tests_linux: &NamedJob) -> NamedJob {
+    fn dispatch_autofix(run_tests_linux_name: &str) -> Step<Run> {
+        let clippy_failed_expr = format!(
+            "needs.{}.outputs.{} == 'true'",
+            run_tests_linux_name, CLIPPY_FAILED_OUTPUT
+        );
+        named::bash(format!(
+            "gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy=${{{{ {} }}}}",
+            clippy_failed_expr
+        ))
+        .add_env(("GITHUB_TOKEN", "${{ steps.get-app-token.outputs.token }}"))
+    }
+
+    let style_failed_expr = format!(
+        "needs.{}.outputs.{} == 'true'",
+        check_style.name, STYLE_FAILED_OUTPUT
+    );
+    let clippy_failed_expr = format!(
+        "needs.{}.outputs.{} == 'true'",
+        run_tests_linux.name, CLIPPY_FAILED_OUTPUT
+    );
+    let (authenticate, _token) = steps::authenticate_as_zippy();
+
+    let job = Job::default()
+        .runs_on(runners::LINUX_SMALL)
+        .cond(Expression::new(format!(
+            "always() && ({} || {}) && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
+            style_failed_expr, clippy_failed_expr
+        )))
+        .needs(vec![check_style.name.clone(), run_tests_linux.name.clone()])
+        .add_step(authenticate)
+        .add_step(dispatch_autofix(&run_tests_linux.name));
+
+    named::job(job)
+}
+
 fn check_dependencies() -> NamedJob {
     fn install_cargo_machete() -> Step<Use> {
         named::uses(
@@ -304,6 +355,8 @@ fn check_workspace_binaries() -> NamedJob {
     )
 }
 
+pub const CLIPPY_FAILED_OUTPUT: &str = "clippy_failed";
+
 pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
     let runner = match platform {
         Platform::Windows => runners::WINDOWS_DEFAULT,
@@ -325,12 +378,24 @@ pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
             )
             .add_step(steps::setup_node())
             .add_step(steps::clippy(platform))
+            .when(platform == Platform::Linux, |job| {
+                job.add_step(steps::record_clippy_failure())
+            })
             .when(platform == Platform::Linux, |job| {
                 job.add_step(steps::cargo_install_nextest())
             })
             .add_step(steps::clear_target_dir_if_large(platform))
             .add_step(steps::cargo_nextest(platform))
-            .add_step(steps::cleanup_cargo_config(platform)),
+            .add_step(steps::cleanup_cargo_config(platform))
+            .when(platform == Platform::Linux, |job| {
+                job.outputs([(
+                    CLIPPY_FAILED_OUTPUT.to_owned(),
+                    format!(
+                        "${{{{ steps.{}.outputs.failed == 'true' }}}}",
+                        steps::RECORD_CLIPPY_FAILURE_STEP_ID
+                    ),
+                )])
+            }),
     }
 }
 

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

@@ -54,8 +54,25 @@ pub fn setup_sentry() -> Step<Use> {
     .add_with(("token", vars::SENTRY_AUTH_TOKEN))
 }
 
+pub const PRETTIER_STEP_ID: &str = "prettier";
+pub const CARGO_FMT_STEP_ID: &str = "cargo_fmt";
+pub const RECORD_STYLE_FAILURE_STEP_ID: &str = "record_style_failure";
+
+pub fn prettier() -> Step<Run> {
+    named::bash("./script/prettier").id(PRETTIER_STEP_ID)
+}
+
 pub fn cargo_fmt() -> Step<Run> {
-    named::bash("cargo fmt --all -- --check")
+    named::bash("cargo fmt --all -- --check").id(CARGO_FMT_STEP_ID)
+}
+
+pub fn record_style_failure() -> Step<Run> {
+    named::bash(format!(
+        "echo \"failed=${{{{ steps.{}.outcome == 'failure' || steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
+        PRETTIER_STEP_ID, CARGO_FMT_STEP_ID
+    ))
+    .id(RECORD_STYLE_FAILURE_STEP_ID)
+    .if_condition(Expression::new("always()"))
 }
 
 pub fn cargo_install_nextest() -> Step<Use> {
@@ -101,13 +118,25 @@ pub fn clear_target_dir_if_large(platform: Platform) -> Step<Run> {
     }
 }
 
+pub const CLIPPY_STEP_ID: &str = "clippy";
+pub const RECORD_CLIPPY_FAILURE_STEP_ID: &str = "record_clippy_failure";
+
 pub fn clippy(platform: Platform) -> Step<Run> {
     match platform {
-        Platform::Windows => named::pwsh("./script/clippy.ps1"),
-        _ => named::bash("./script/clippy"),
+        Platform::Windows => named::pwsh("./script/clippy.ps1").id(CLIPPY_STEP_ID),
+        _ => named::bash("./script/clippy").id(CLIPPY_STEP_ID),
     }
 }
 
+pub fn record_clippy_failure() -> Step<Run> {
+    named::bash(format!(
+        "echo \"failed=${{{{ steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
+        CLIPPY_STEP_ID
+    ))
+    .id(RECORD_CLIPPY_FAILURE_STEP_ID)
+    .if_condition(Expression::new("always()"))
+}
+
 pub fn cache_rust_dependencies_namespace() -> Step<Use> {
     named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust"))
 }
@@ -344,3 +373,16 @@ pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step<Run> {
         "git fetch origin {ref_name} && git checkout {ref_name}"
     ))
 }
+
+pub fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
+    let step = named::uses(
+        "actions",
+        "create-github-app-token",
+        "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
+    )
+    .add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
+    .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
+    .id("get-app-token");
+    let output = StepOutput::new(&step, "token");
+    (step, output)
+}