Compare commits

..

197 commits

Author SHA1 Message Date
morgankrey
5f8a7413a3
Update Zed-hosted model documentation (#60771)
Some checks are pending
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
## Summary

- Document pricing and Zed-hosted context limits for Claude Fable 5,
Claude Sonnet 5, and GPT-5.6 Sol, Terra, and Luna.
- Record recent Gemini and xAI model retirements and replacement
guidance.
- Put the Fable safety-retention warning on the hosted-model reference
and align the privacy docs with the current Anthropic Covered Models
terminology and retention period.

## Sources

- zed-industries/cloud origin/main at
f5d109a868a303241e89ea30d6da8da19699ead4
- OpenAI GPT-5.6 model documentation
- Anthropic Covered Models retention policy
- xAI May 15, 2026 model retirement guide

## Testing

- pnpm dlx prettier@3.5.0 . --check
- mdbook build docs

Release Notes:

- N/A
2026-07-10 23:24:52 +00:00
Cameron Mcloughlin
8f92822cbf
agent: Sandbox security review and docs update (#60291)
Closes security loopholes and updates docs:
- installs seccomp filter for blocking naughty syscalls
- tightens macos seatbelt profile
- fetch tool responses that redirect are now constrained by allowed
domains list

Also adds a few "Learn More" buttons that link to the new docs.

Also fixes a bug where the agent would try to create a
`~/.config/zed/AGENTS.md` directory

Also adds unicode confusable detection to URL/path privilege escalation
prompts.

---

Release Notes:

- N/A or Added/Fixed/Improved ...

---------

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
2026-07-10 22:51:41 +00:00
MartinYe1234
7344a31ddc
agent: Fix edit_file corrupting indentation when old_text omits first-line indent (#60613)
The `edit_file` tool re-indents `new_text` by computing a single indent
delta from the first line of `old_text` versus the matched buffer line,
then applying that delta to every line of the replacement. When a model
omits the leading indentation on only the first line of
`old_text`/`new_text` (a common pattern when copying from mid-line
context), the delta computed from the first line was wrongly applied to
the remaining, already-correctly-indented lines, doubling their
indentation.

This PR:

- Tracks the `(query_row, buffer_row)` line pairs aligned by the
streaming fuzzy matcher so the indent of each `old_text` line can be
compared against the buffer line it actually matched.
- Computes a separate indent delta for the lines after the first: when
those lines agree on a consistent delta, it's used for the rest of the
replacement; otherwise the previous uniform behavior is preserved.
- Keeps `query_lines`/line pairs in sync when `finish()` extends a match
with a trailing incomplete line.
- Adds an end-to-end regression test reproducing the issue, plus unit
tests for the new re-indentation logic.

Closes https://github.com/zed-industries/zed/issues/60302

Release Notes:

- Fixed the agent's `edit_file` tool corrupting indentation when a
replacement omitted leading whitespace on only its first line.
2026-07-10 20:21:26 +00:00
Lukas Wirth
2b9b3c7ea2
worktree: Watch .git/refs subdirectories for external ref updates (#60660)
On Linux and FreeBSD the native file watcher is non-recursive, so a
watch on the .git directory itself does not report changes to files
nested below it. Loose refs live in nested directories under refs, so
external git commit, fetch, branch, and update-ref operations that don't
also touch a direct child of .git (like the index) went entirely
unnoticed.

Watch every directory in the refs tree when a repository is inserted,
and watch directories subsequently created under refs (new remotes,
slash-named branches) as their creation events arrive. On platforms with
recursive watchers these registrations dedupe against the existing
recursive watch, making them free.


---

Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-10 17:29:20 +00:00
Zephiris Evergreen
01f915d6be
picker: Automatically switch layout depending on window dimensions (#60559)
# Objective

Fixes #59820. 

## Solution

At a high level, I approached this by conditionally rendering a `Below`
preview layout only when the preview was at or below the min width of
the `Right` when the layout is `Right`. Since there's one breakpoint at
`SizeBounds::min_width()` and hitting that breakpoint only changes
rendering (and doesn't change the state), there's no need to persist to
storage or memory that the view is forced to be Below.

I toggled between the two rendering modes by defining
`Picker::preview_layout_rendered()` that would return `Below` if the
window was equal to or smaller than its min width. Then I replaced all
calls of `preview_layout()` that used the response purely for rendering
purposes with `preview_layout_rendered()`. I avoided replacing
`preview_layout()` calls that were responsible for persisting the
current layout and window dimensions to disk to ensure that changes to
the window dimensions were properly synced to the correct layout. This
way changes to the window while in the `BelowAuto` rendering mode edit
the `Right` window dimensions on disk.

## Testing

I tested these changes manually with the following actions on my local
machine (an Apple Silicon Macbook Pro):
- resizing the popover
- resizing the window
- adjusting the split
- making sure the popover size and split percent remains the same after:
  - closing and re-opening the popover
- closing and re-opening the application and making sure the window size
and split percent remains the same
  
Reviewers can test my changes by performing the same manual actions.

## Self-Review Checklist:

- [X] I've reviewed my own diff for quality, security, and reliability
- [X] Unsafe blocks (if any) have justifying comments
- [X] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [X] Tests cover the new/changed behavior
- [X] Performance impact has been considered and is acceptable

## Showcase

### Before


https://github.com/user-attachments/assets/a4524065-d1a3-4595-bc9e-50c9715b4e66

### After


https://github.com/user-attachments/assets/fede7eb0-5d73-423c-8df0-8192caee8d26

---

Release Notes:

- fix(picker): Automatically switch from Right split to Down split when
picker gets too narrow
2026-07-10 16:52:14 +00:00
Gabriele Ancillai
04d03d1fe5
lmstudio: Fix context wheel by including token usage in streaming responses (#57861)
Some checks are pending
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
LM Studio doesn't show the context token wheel (#53790) because token
usage is
never reported in streaming responses.

Causes:

1. `stream_options` was missing from the request. Without
`stream_options: { include_usage: true }`, the LM Studio API omits
`usage`
   from every streaming chunk entirely.

2. The event mapper discarded usage data in the final chunk.
OpenAI-compatible
servers send the usage summary in a trailing chunk that has an empty
`choices`
array. The old guard treated that as an error, so even when usage was
present
   it was thrown away before emitting a `UsageUpdate` event.

Fix:
- Add `StreamOptions { include_usage: bool }` and `stream_options` to
`ChatCompletionRequest`, and always set it to `true` for streaming
requests.
- Move usage handling in `LmStudioEventMapper::map_event` to run
*before* the
  empty-choices guard, mirroring the OpenAI provider's approach.
- Add four unit tests for `map_event` covering the fixed behavior.

Release Notes:

- Fixed LM Studio not showing the context window usage wheel. 

<img width="1184" height="1080" alt="Screenshot_20260527_130449"
src="https://github.com/user-attachments/assets/97eb8500-39dd-4824-aaf8-f0422b62119d"
/>

---------

Co-authored-by: Gabriele Ancillai <gabriele.ancillai@sofka.com.co>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2026-07-10 15:32:21 +00:00
Nakano Kenji
bd2e879213
anthropic: Allow configured models to opt into fast mode (#58225)
Adds support for enabling Anthropic fast mode on configured models.

Previously, Anthropic models configured through
`language_models.anthropic.available_models` were always marked as not
supporting fast mode, so `speed: "fast"` would be stripped before
sending requests even when the configured model supported Anthropic fast
mode.

This adds an optional `supports_fast_mode` field to configured Anthropic
models. When enabled, the model is marked as supporting fast mode and
the required Anthropic beta header is added automatically. Built-in
fast-mode model detection remains the fallback when the setting is
omitted.

Testing:
- `cargo fmt --package anthropic --package language_models --package
settings_content`
- `cargo test -p language_models available_model --lib`
- `cargo test -p anthropic from_listed_enables_fast_mode --lib`
- `cargo check -p language_models`
- Manual: verified in a local build that a configured Anthropic model
can send fast mode requests correctly.

Release Notes:

- agent: Allow specifying if fast mode is supported for custom anthropic
models

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2026-07-10 15:01:47 +00:00
Tyler Benfield
503292376e
Fix remote worktree picker hides "Create from origin/main" option (#59134)
This passes the `include_remote_name` flag through the remote
`GetDefaultBranch` RPC instead of dropping it at the client/host
boundary. That lets remote repositories resolve default branches as
`origin/main` when needed, so worktree creation uses a valid remote
branch name instead of falling back to `main`.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #59121

Release Notes:

- Fixed remote worktree creation from the default branch when the
default branch requires its remote name.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-10 12:11:57 +00:00
Roman Cinis
c5383ce5d4
extension_api: Expose hard_tabs in LanguageSettings (#52560)
Closes #21822
The issue is closed already, but it was requested to provide the
`hard_tabs` boolean param.

Follow-up to #52175
Reviewed by @maxdeviant (it already bumped the settings path to v0.8.0,
so this `hard_tabs` change only needs 2 files, 2-3 lines of code
additions).


## Context

Exposes `hard_tabs` from `AllLanguageSettings` to the Extension
API's `LanguageSettings` struct. Currently, only `tab_size` and
`preferred_line_length` are available to extensions, which prevents
language extensions (e.g., Go, C++) from reading the user's hard tabs
preference and forwarding it to their language server or formatter
(e.g., as rustfmt.hard_tabs or clang-format.UseTab).

Related:
https://github.com/zed-industries/zed/issues/21822#issuecomment-4139809956

## How to Review

Small change — follow how `tab_size` is plumbed through:
1. WIT definition (`language-settings` record)
2. `extension_api` Rust struct
3. Host-side bridge conversion

The new field follows the exact same pattern.

## Self-Review Checklist

<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
  - Compile-time verified via WIT bindings; no runtime behavior change
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A
2026-07-10 11:51:16 +00:00
Sergey Borovikov
0953838ec2
javascript, typescript: Inject GLSL and WGSL syntax in comment-labeled template literals (#55341)
Extends the existing ECMAScript comment-label injection system to
support GLSL and WGSL shader languages inside JS/TS strings and template
literals.

The pattern `/* language */` before a string is already used for `html`,
`sql`, `graphql`, and `css`. This adds the same support for two shader
languages commonly embedded as strings in WebGL/WebGPU code:

before:
<img width="460" height="300" alt="Screenshot 2026-04-30 at 23 07 05"
src="https://github.com/user-attachments/assets/8952eddc-2f53-42ea-b086-7a0b63dceb10"
/>

after:
<img width="460" height="300" alt="Screenshot 2026-04-30 at 23 07 39"
src="https://github.com/user-attachments/assets/93bf536a-9df4-438b-af7b-382efda196ca"
/>

Syntax highlighting activates when the corresponding extension is
installed:
- GLSL: the built-in `glsl` extension
- WGSL: the community extension
[`wgsl-wesl-zed`](https://github.com/lucascompython/wgsl-wesl-zed)
(language name `WGSL/WESL`)

Both `/*glsl*/` and `/* glsl */` forms are accepted (same behavior as
the existing patterns).

Release Notes:

- Added `/*glsl*/` and `/*wgsl*/` comment-label syntax injection for
JavaScript and TypeScript template literals
2026-07-10 11:45:13 +00:00
Bennet Bo Fenner
585e4aab08
agent: Fix cmd-f not working when buffer is open (#60750)
Turns out I only tested it with all tabs closed, the keybinding would
only work when all tabs were closed and you focused the agent panel. If
a file was opened in the center pane, cmd-f would open a search in that
file instead of searching in the agent panel, even if the thread view as
focused.

Closes #60686

Release Notes:

- agent: Fixed an issue where cmd-f would not work if file is open in
center pane
2026-07-10 11:38:53 +00:00
saberoueslati
5cf706ceeb
terminal_view: Forward ctrl-q to PTY when terminal is focused on Linux (#58879)
## Context

On Linux, `ctrl-q` is globally bound to `zed::Quit`. When a
`TerminalView` is focused, pressing `ctrl-q` quit the application
instead of forwarding the keycode to the shell, breaking programs like
`ftp`, `tig`, and any app that uses XON/XOFF flow control.

Windows already had the fix: its `Terminal` keymap context overrides
`ctrl-q` with `["terminal::SendKeystroke", "ctrl-q"]`. The Linux keymap
was simply missing that override.

Closes #58809

Manual test after fix below :

[Screencast from 2026-06-09
00-46-37.webm](https://github.com/user-attachments/assets/3d103b2a-bff1-4559-af1d-2a52d57a6b18)


## How to Review

- **`assets/keymaps/default-linux.json`** : One-line addition in the
`Terminal` context under the "Overrides for conflicting keybindings"
comment, mirroring the existing Windows entry.
- **`crates/terminal_view/src/terminal_view.rs`** : Regression test
`ctrl_q_is_forwarded_to_terminal_not_quit` (Linux-only, `#[cfg(target_os
= "linux")]`): loads the default keymap, focuses a display-only
terminal, simulates `ctrl-q`, and asserts the PTY receives byte `0x11`
instead of the quit action firing.

## Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed `ctrl-q` quitting Zed instead of being forwarded to the shell
when a terminal is focused on Linux
2026-07-10 11:29:50 +00:00
Ben Brandt
fbceb2823b
acp: Enable ACP elicitations (#60749)
Removes the feature flag. The RFD is in Preview and I am confident it
will be stable without major changes by the time this hits Zed Stable.

Release Notes:

- acp: Allow ACP agents to use Elicitation capturing structure user
input.
2026-07-10 11:22:16 +00:00
David Wu
60314a7416
Open non-writeable files in Capability::Read mode (#57202)
Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #57174

Release Notes:

- Open non-writeable files in Capability::Read mode

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2026-07-10 09:50:19 +00:00
Richard Boisvert
af56cb75a8
agent_ui: Fix duplicate terminal when starting a new agent thread (#59586)
# Objective

- Fixes #58097.
- Opening a new project and clicking `+` in the agent panel to start a
terminal thread creates two terminals instead of one. I am able to
replicate the issue on version 1.8.0 on macOS 27

## Solution

The new-thread action creates the terminal and then focuses the agent
panel. Focusing re-activates the panel (`Panel::set_active` then
`ensure_thread_initialized`) before the terminal, which is spawned
asynchronously, has registered. The panel still looks uninitialized at
that moment, so it spawns its own "initial" terminal too, and that is
the duplicate.

`spawn_terminal` now marks the spawn as in-flight
(`pending_terminal_spawn`) the moment it starts, the same way the
restore and initial-terminal paths already do, so the existing guard in
`ensure_thread_initialized` skips the redundant terminal.

This only affected new (unrestored) projects, since existing ones
restore their previous entry instead of auto-creating one. The auto-init
behavior was introduced in #57150.

## Testing

- Verified in a local dev build on macOS: opening a fresh project and
clicking `+` now creates one terminal, and clicking `+` again creates a
second, as expected. Reopening an existing project still restores a
single terminal.
- Added `test_explicit_terminal_blocks_redundant_auto_init`, which fails
without the fix.
- The change is platform-agnostic (no platform-specific code); I wasn't
able to test on Linux/Windows.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed a duplicate terminal being created when starting an agent
terminal thread in a new project
2026-07-10 09:36:11 +00:00
Om Chillure
b2db24e58a
Fix MCP servers in multi root workspaces (#52849)
### Closes #51951

## Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

#### Note : Reopens previous work from closed PR
https://github.com/zed-industries/zed/pull/52161 (fork was deleted)

## Video 

[Screencast from 2026-03-22
23-26-06.webm](https://github.com/user-attachments/assets/ab68e47a-7e74-4f1e-991d-8ca4fed7952c)


## Release Notes:

- Fixed MCP servers from `.zed/settings.json` not being discovered when
multiple project folders are open in a workspace.

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2026-07-10 09:13:45 +00:00
Bennet Bo Fenner
9db37846a6
agent: Add GPT 5.6 Sol/Terra for ChatGPT subscription (#60743)
Some checks are pending
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Release Notes:

- agent: Added GPT 5.6 Sol & Terra for ChatGPT subscription. Note: GPT
5.6 Luna is not available yet, since OpenAI has not unlocked access for
third-party clients
2026-07-10 08:10:51 +00:00
Joseph T. Lyons
76c93968da
Fix workspace double-lease panic when reusing Git Graph (#60717)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
Release Notes:

- Fixed a panic when running “Show in Git Graph” while the Git Graph was
already open.
2026-07-09 23:51:04 +00:00
Dino
a5f696bfa3
agent_ui: Remove duplicate MCP Servers menu item (#60712)
# Objective

Fixes https://github.com/zed-industries/zed/issues/60709 by removing the
duplicate menu section for "MCP Servers".

## Solution

Update `agent_ui::agent_panel::AgentPanel::render_panel_options_menu` to
ensure the "MCP Servers" section is only rendered once, if not using a
Terminal Thread.

## Testing

Tested manually, comparing against the stable release, as this bug is
present in Preview. Screenshot is shown in the "Showcase" section.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable

## Showcase

<details>
  <summary>Before</summary>
<img width="2736" height="1586" alt="CleanShot 2026-07-09 at 23 14
34@2x"
src="https://github.com/user-attachments/assets/ce2014db-bd72-46fb-bfa1-247cd5471daf"
/>

</details>

<details>
  <summary>After</summary>
<img width="2736" height="1586" alt="CleanShot 2026-07-09 at 23 15
13@2x"
src="https://github.com/user-attachments/assets/bbba9965-3f6a-4137-94e5-17704efd2cce"
/>

</details>


---

Release Notes:

- N/A
2026-07-09 22:19:08 +00:00
John Tur
2c4e44704c
Fix "Task polled after completion" panic (#60693)
Dropping a scheduled runnable cancels its task and makes the next poll
of any awaiter panic with "Task polled after completion." The only paths
where we drop these runnables seem to be during shutdown or extreme
resource exhaustion, so, let's leak the runnables instead of crashing.

On Windows, we also moved to calling the Win32 thread pool API directly,
because 1) WinRT thread pool API is just a wrapper that adds overhead we
don't need, and 2) the closure we pass to the `WorkItemHandler` object
takes ownership of the runnable object, so if the WinRT thread pool
releases the delegate, it can free the runnable without our control.

Release Notes:

- N/A
2026-07-09 18:06:00 +00:00
Revantark
35ffa8f480
git_ui: Fix history tab empty and detached HEAD states (#57959)
This PR fixes a few History tab edge cases in the Git Panel.

For a fresh repo with no commits, the History tab now finishes loading
and shows No commits yet instead
of sitting on Loading… indefinitely or falling into a misleading
empty/error state.

It also fixes detached HEAD history loading. In that case, the Git Panel
asks the backend to load history
from the current commit SHA. The local git backend was accidentally
treating the raw object ID bytes as a
string instead of formatting them as a normal hex SHA, so git log could
fail before returning any
commits. The backend now passes the SHA in the format git expects.

**Repro for empty repo:**

mkdir /tmp/zed-empty-history
cd /tmp/zed-empty-history
git init
zed .

Open Git Panel → History.

Before: History could stay stuck on Loading….
After: History shows No commits yet.

**Repro for detached HEAD:**

mkdir /tmp/zed-detached-history
cd /tmp/zed-detached-history
git init
echo hi > file
git add file
git commit -m initial
git checkout --detach HEAD
zed .

Open Git Panel → History.

Before: History could fail to load commits.
After: History shows the commit history normally.




Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable


Release Notes:

- Fixed Git history tab states for empty repositories and detached HEAD
history.

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-09 17:51:16 +00:00
Bennet Bo Fenner
d8c8040462
open_ai: Add GPT 5.6 models (#60697)
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>

Add new GPT 5.6 models

Release Notes:

- open_ai: Added support for GPT 5.6 Sol/Terra/Luna

---------

Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
2026-07-09 17:37:43 +00:00
Bennet Bo Fenner
3c45bfb205
agent_ui: Allow expanding running MCP tool calls (#60695)
Closes #57144

Release Notes:

- agent: Allow expanding in-progress MCP tool calls

Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
2026-07-09 16:49:57 +00:00
Danilo Leal
717e1c0590
Make agent diff tab consistent with commit view tab (#60470)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
This makes them more consistent with regards to limiting the label size.
These two tabs are similar because they frequently house a label that's
pretty big, and the agent diff tab didn't truncate it in any way.

Release Notes:

- N/A
2026-07-09 13:41:58 +00:00
Lena
da4703be85
Dedupe first-responder Slack notifications (#60666)
What looks like a single event of applying a few labels is actually
multiple events, and the raciness is racy as we've recently experienced.
One solution would've been adding a “first responders notified” label
for enforcing consistency but issues already have enough labels (and we
could take it off by mistake), so a bot reaction will instead serve as a
marker for any later runs.

So if we're applying multiple labels or changing our mind about, say, an
area label after the notification has already been sent, this should
work fine now, without duplicate notifications.

Release Notes:

- N/A
2026-07-09 13:39:43 +00:00
Lukas Wirth
11d216d8bf
worktree: Refresh all repositories sharing a changed git directory (#60664)
update_git_repositories mapped a changed .git path to a repository with
find_map, so when several repositories share a git directory - a main
checkout plus one of its linked worktrees in the same project worktree -
a ref update under the shared common dir only bumped git_dir_scan_id on
whichever repository iterated first, leaving the others stale until an
unrelated event happened to refresh them. Collect every matching
repository and bump each one.

---

Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-09 12:41:50 +00:00
Lukas Wirth
e2d41c477c
fs: Retry watch registrations skipped during the native watch-limit cooldown (#60662)
GlobalWatcher::add returns Ok(None) while the native watch-limit
cooldown is active, and FsWatcher::add_existing_path treated that as
success: the path was never watched, with no retry and no error. A
long-lived watch (like a repository's git directory) that happened to
register during a cooldown window silently never received events.

Route the skipped registration through the existing pending-path
machinery, which already polls until registration succeeds and emits a
rescan event for the path so that changes missed in the interim are
picked up.

---

Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-09 12:39:43 +00:00
Elliot Thomas
8230cb16d1
project_panel: Restore worktree drag-and-drop reordering (#55755)
Reordering worktree roots by drag-and-drop had silently broken: a
worktree-root filter added to `disjoint_entries` for delete-safety
stripped roots before the drop handler could see them, so root-to-root
drops never reached the existing `move_worktree` reorder path.

This PR is the minimal regression fix:

- Move the worktree-root filter out of `disjoint_entries` and into
`disjoint_effective_entries` (used by cut/copy/delete), so drag-and-drop
keeps seeing roots and single-root reorder works again via the existing
`move_worktree` path.
- Filter worktree roots out of `drag_onto`'s copy branch, so holding the
copy modifier over a drag that contains a root no longer returns `None`
from `create_paste_path` and silently cancels the whole copy.
- Add `test_drag_worktree_root_reorders_worktrees` exercising the
drag-onto reorder flow end to end.

The larger feature work (multi-root group reordering, blank-area "send
to end", copy-mode drag feedback, and syncing worktree order to
collaborators) has been split into a separate follow-up PR so this fix
can land quickly. Note that worktree order was intentionally not synced
during collaboration, so that change is discussed separately.

Closes #46699

Release Notes:

- Fixed drag and drop to reorder worktrees

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-09 12:32:00 +00:00
Ben Kunkle
7828463fcb
edit_prediction: Report patch apply failures (#60637)
# Objective

- Report V4 edit prediction patch apply failures as cloud rejection
events.
- Closes EP-210

## Solution

- Added `PatchApplyFailed` as an edit prediction rejection reason.
- Converted `prediction_edits_for_single_file_diff` errors into rejected
prediction results so the existing rejection pipeline posts them to
`/predict_edits/reject`.
- Kept interpolation failures reported as `InterpolateFailed`.

## Testing

- Ran `cargo fmt --check && cargo check -p edit_prediction`.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A
2026-07-09 12:28:59 +00:00
Ben Kunkle
3e976e89bc
language_model: Add OpenAI Responses API custom tool support (#60638)
# Objective

Allow zed's language model stack to express OpenAI Responses API custom
tools (freeform text-input tools with an optional lark/regex grammar),
so downstream consumers can offer tools like a freeform `apply_patch` to
GPT models.
## Solution

- `LanguageModelRequestTool` now carries a Function-vs-Custom input
variant; `LanguageModelCustomToolFormat` models text/grammar formats.
- `LanguageModelToolUse.input` becomes a typed
`LanguageModelToolUseInput::{Json, Text}`. Serialization is tagged so
persisted Text inputs round-trip losslessly; legacy plain JSON values
still deserialize as `Json`.
- `open_ai` gains the custom tool wire types (tool definition,
`custom_tool_call`/`custom_tool_call_output` input items with
string-or-content-part outputs, output item, and
`custom_tool_call_input` delta/done stream events). The Responses event
mapper accumulates raw text deltas into `ToolUse` events, and history
replay derives custom-vs-function tool results from the matching
`ToolUse` by id.
- All non-OpenAI providers and the Chat Completions path error
explicitly when a request contains custom tools — no silent drops or
empty-schema coercions.

Release Notes:

- N/A
2026-07-09 12:05:50 +00:00
Finn Evers
55b776183d
ci: Fix push permissions for cherry-pick workflow (#60673)
Release Notes:

- N/A
2026-07-09 11:34:57 +00:00
Finn Evers
9b4598f708
ci: Fix insufficient permissions for asset validation step (#60625)
Release Notes:

- N/A
2026-07-09 11:19:07 +00:00
Smit Barmase
8f907c5ee1
markdown: Improve selection copying to always yield valid markdown (#60657)
Closes https://github.com/zed-industries/zed/issues/59083
Closes https://github.com/zed-industries/zed/issues/42958
Closes https://github.com/zed-industries/zed/issues/59612

This PR improve selection copy of Agent Panel and Markdown Preview such
that copied text is always valid markdown, following how VSCode handles
it. The partial selection of styled text would not copy broken syntax
like `bold**` or ```inline code` ``` like before.

Now we follow simple rule:

> Selecting any part, no matter from where, copies it as its markdown.
Except when the selection sits entirely inside a single inline code
span, in which case we copy plain text, for terminal and code use cases.

Examples:

This is **bold** text, this is *italic* text, and this is `code` all `in
one` sentence.

- selecting only bold → `**bold**`
- selecting normal text and partial bold → `is is **bo**`
- selecting a single code span completely → `code`
- selecting partial code → `od`
- selecting partial text and code → `` his is `cod` ``
- selecting multiple code spans partially → `` `ode` all `in o` ``
- selecting multiple code spans end to end → `` `code` all `in one` ``

Nested spans, like **bold with `code` inside**:

- partial text in bold → `**ld wi**`
- partial code in bold → ``**`od`**``
- full code in bold → ``**`code`**``
- the whole sentence → ``**bold with `code` inside**``

Links, like [Visit Rust's website](https://rust.org):

- partial link text → `[bsite](https://rust.org)`
- full link text → `[Visit Rust's website](https://rust.org)`

How it works:

Selection boundaries that land inside delimiter syntax (`**`, backticks,
etc.) first snap out so no delimiter is left half-selected. Then any
spans the selection cuts through get their delimiters re-added,
outermost first, so nested styling stays balanced. Only the root blocks
containing the two selection boundaries are inspected, everything in
between is copied right as is, which also keeps this cheap on large
documents.

Release Notes:

- Improved copying selected text in Agent Panel and Markdown Preview.
Partial selections of styled text now copy as well-formed markdown, and
selections within a single inline code span copy as plain text.
2026-07-09 11:05:04 +00:00
Lukas Wirth
9cdaff1492
editor: Fix panic removing blocks in split diff with companion edit at EOF (#60445)
BlockMap::sync unwrapped the transform cursor for every edit, assuming
each edit's old.start lands strictly inside the old transform tree. The
companion (split-diff) branch of sync can compose an edit anchored at
the trailing boundary of the old transforms, leaving the cursor past the
end of the tree and aborting the process on the None unwrap. Only bind
the transform when there are rows preceding the edit.

Fixes ZED-9V4.
Closes FR-113


Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-09 09:20:39 +00:00
Lukas Wirth
0bc9c99f00
Invalidate hover state when a multibuffers paths change (#60443)
Fixes a panic when multibuffer paths change under a cursor hover
Fixes ZED-9W1
Closes FR-112

Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-09 09:20:36 +00:00
Bennet Bo Fenner
4d6762ad6b
cli: Restore workspace by default when cli_default_open_behavior="new_window" (#60652)
Closes #60325, Follow up to #59415

Fixes an issue where launching Zed from the cli (just `zed`, no path)
would not restore the previous window when using
`cli_default_open_behavior: new_window`

Release Notes:

- cli: Fixed an issue where the previous workspace would not be restored
when using `cli_default_open_behavior: new_window` and no path was
provided
2026-07-09 08:20:29 +00:00
Vitaly Slobodin
c93b4cd9cc
ruby: Mention kanayago and fuzzy-ruby-server in the documentation (#60640)
# Objective

Hi! This PR updates the Ruby doc to mention 2 language servers
`kanayago` and `fuzzy-ruby-server`.

## Testing

N/A

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

N/A

---

Release Notes:

- N/A
2026-07-09 07:54:36 +00:00
Todd L Smith
57261fef89
editor: Add actions to move between comment paragraphs (#58353)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
Congratsbot / congrats (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Jump the caret straight to the next/previous block of prose comments —
like paragraph motion, but for comments.

The motivating workflow: reflowing a file's comments with `editor:
rewrap`. Today that means reaching for the mouse to click into each
comment block scattered through the file, or using other cursor motions
and undershooting or overshooting the target. With these two actions you
can step from one comment paragraph to the next, `rewrap`, and repeat —
reflowing every comment in a file without ever touching the mouse. It's
also handy for skimming a heavily-documented file: hop from doc comment
to doc comment without manually scrolling past the code in between.

Adds two editor actions:

- `editor::MoveToNextCommentParagraph`
- `editor::MoveToPreviousCommentParagraph`

Both move the caret to the first non-whitespace character of the
next/previous *comment paragraph*. They have no default keybinding and
are available from the command palette ("editor: move to next/previous
comment paragraph").

### What counts as a comment paragraph

A comment paragraph is a run of consecutive comment lines. A line is a
comment line when its **first non-whitespace character is in a `comment`
syntax scope** and the line contains prose (at least one alphanumeric
character). This is determined from the syntax tree
(`language_scope_at(...).override_name()`), the same mechanism `rewrap`
and comment folding already use, so it behaves correctly without
per-language string matching:

- **End-of-line comments preceded by code are ignored** — on `let x = 1;
// note` the first non-whitespace character is code, not a comment, so
the line is not a paragraph line.
- **`//` inside a string literal is ignored** — its scope is `string`,
not `comment`.
- **Blank/divider comment lines separate paragraphs** — a bare `//` or
`// -----` (no prose) acts as a separator, so you can hop between
paragraphs *within* one comment block as well as across blocks.

Both directions always move to a paragraph *other* than the one the
caret is in: when the caret is inside a paragraph, the whole current
paragraph is skipped, so `Prev` lands on the previous paragraph's start
rather than the current paragraph's own start.

### On the autoscroll

These two actions scroll the destination near the top of the viewport
(`Autoscroll::top_relative`) rather than using the default `Fit`
strategy that sibling motions use. This is deliberate and specific to
the feature: you are jumping to the **start** of a comment paragraph
that extends *downward*, so biasing the destination toward the top keeps
the whole paragraph visible after the jump. This matters for the rewrap
workflow above — you want to see the full comment you are about to
reflow, and the reflow itself changes the paragraph's line count. With
the default `Fit`, repeated forward jumps creep the caret to the bottom
edge and leave long paragraphs cut off below the fold — the opposite of
what this motion is for. Happy to revisit the exact strategy/offset if
you'd prefer consistency with the other motions.

### Tests

Two tests in `editor_tests.rs` (using a real grammar + comment override
query):

- `test_move_to_next_and_previous_comment_paragraph` — full
forward/backward round trip, covering blank comment-line separators,
code separators, trailing comments, and the no-more-paragraphs stop.
- `test_move_to_previous_comment_paragraph_skips_current_paragraph` —
`Prev` from mid-paragraph skips to the previous paragraph, and stays put
when there is no previous paragraph.

---

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments — N/A, no unsafe
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Added `editor::MoveToNextCommentParagraph` and
`editor::MoveToPreviousCommentParagraph` actions to move the caret
between comment paragraphs
2026-07-09 05:49:09 +00:00
Richard Feldman
f281770034
gpui: Store GPU-facing bools as PaddedBool32 to avoid uninitialized padding (#60482)
`PolychromeSprite` in `crates/gpui/src/scene.rs` is `#[repr(C)]` and had
a `grayscale: bool` field followed by 3 compiler-inserted padding bytes
that were never written. The wgpu renderer's `instance_bytes`
reinterprets `&[PolychromeSprite]` as `&[u8]` via
`slice::from_raw_parts` and passes it to `queue.write_buffer`, so those
uninitialized padding bytes were exposed behind a shared `&[u8]` on
every frame that draws an image or emoji, which is undefined behavior.

Rather than widening the field to a raw `u32` (which would suggest
values other than 0 and 1 are meaningful), this introduces
`PaddedBool32`: a `#[repr(transparent)]` wrapper around `u32` whose only
public constructor is `From<bool>`, so the 0-or-1 invariant is enforced
by the type while the layout has no padding. `Underline.wavy`, which was
already a raw `u32` for the same reason, is converted too.

cbindgen emits the wrapper as `typedef uint32_t PaddedBool32;`, so the
generated Metal header and shaders are unchanged. The WGSL and HLSL
shaders already declared these fields as `u32`/`uint`; their `& 0xFFu`
masks, which existed to ignore the garbage padding bytes, are now
simplified to plain comparisons.

Release Notes:

- N/A
2026-07-09 05:37:35 +00:00
morgankrey
10504e3ce1
Improve docs AI readiness (#59577)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Context

This PR makes the Zed docs easier for AI tools, search crawlers, and
users to consume without changing the visible docs content. The current
production docs are primarily optimized for browser navigation. They do
not expose first-class Markdown URLs, an `llms.txt` index, page-level
copy affordances, or machine-readable freshness metadata that let users
and agents grab clean, current page content.

Changes

- Generate Markdown copies for docs pages during the mdBook postprocess
step, including `/docs/index.md` as an alias for Getting Started.
- Generate `/docs/llms.txt` from the mdBook chapter list, grouped by
`SUMMARY.md` sections and annotated with page frontmatter descriptions.
- Generate `/docs/sitemap.xml` with `<lastmod>` values for every docs
page.
- Emit machine-readable freshness metadata in HTML via `last-modified`
and `article:modified_time` meta tags.
- Generate Cloudflare Pages `_redirects` for `.html`, extensionless, and
`.md` redirect variants, with channel-aware docs destinations.
- Add discovery hints for agents and crawlers: `rel="llms.txt"`,
`rel="alternate" type="text/markdown"`, and a short generated `llms.txt`
directive in copied Markdown pages.
- Update the docs proxy so `Accept: text/markdown`, `/docs.md`, and
direct `.md` requests can resolve to the generated Markdown artifacts.
- Move primary docs content earlier in the HTML source while preserving
the visible layout, so crawlers and agent scorers encounter the article
before sidebar chrome.
- Move the existing copy-as-Markdown control from the top navigation
into the page-title row, using the generated Markdown alternate link as
the source of truth.
- Split AI-discovery artifact generation out of
`docs_preprocessor/src/main.rs` into a focused module.

Best Practices Adopted

- Use `llms.txt` as a concise navigation index, not a dump of full page
content.
- Link to absolute, canonical Markdown URLs from `llms.txt`.
- Preserve the docs hierarchy in `llms.txt` instead of emitting a flat
sitemap-like list.
- Include short per-link descriptions from existing metadata rather than
inventing summaries.
- Keep `llms.txt`, Markdown copies, sitemap data, redirects, and
freshness metadata generated from the same mdBook source to avoid drift.
- Advertise Markdown alternates with standard HTML metadata and
same-origin URLs.
- Support both explicit Markdown URLs and content negotiation for
clients that prefer Markdown.
- Keep browser copy behavior pointed at generated Markdown alternate
links instead of duplicating route inference in JavaScript.
- Keep the copy-as-Markdown affordance in the page title row without
duplicating header chrome controls.

Validation

- `cargo check -p docs_preprocessor`
- `cargo test -p docs_preprocessor`
- `./script/clippy -p docs_preprocessor`
- `mdbook build ./docs --dest-dir=../target/deploy/docs/`
- `node --check docs/theme/plugins.js`
- `pnpm dlx prettier@3.5.0 docs/theme/plugins.js --check`
- `git diff --check`
- Local artifact checks confirmed generated Markdown pages, `llms.txt`,
`sitemap.xml` lastmod values, HTML freshness metadata, Markdown
alternate links, redirect targets, and preprocessed action/keybinding
tags resolve as expected.
- Worker URL rewrite mock covered `/docs/`, `/docs.md`,
`/docs/index.md`, extensionless docs routes, `.html` routes,
`/docs/llms.txt`, and `/docs/sitemap.xml`.
- High-effort adversarial subagent review found blockers around
channel-aware redirects, shallow-checkout date fallback, file size,
duplicated Markdown path inference, and process spawning. Those were
addressed.

Remaining Notes

- Local `python -m http.server` does not emulate Cloudflare Pages pretty
URLs, `_redirects`, or the docs-proxy Worker, so full local `afdocs`
still cannot prove content negotiation end to end.
- Existing production remains unchanged until this PR is deployed
through the docs workflow.
- Production baseline `npx afdocs check https://zed.dev/docs/ --fixes
--verbose` still reports the original failures before this PR is
deployed: 12 passed, 8 failed, 3 skipped.

Release Notes:

- Improved docs AI-readiness by adding machine-readable discovery,
Markdown access, and freshness metadata.

---------

Co-authored-by: Katie Geer <katie@zed.dev>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2026-07-08 20:54:51 +00:00
Richard Feldman
dd68454633
Update wayland-backend to fix Wayland file dialog crash (#60621)
On Wayland, closing a window in the brief gap between requesting a
portal file dialog and ashpd exporting the window's surface (used to
parent the dialog) crashed Zed with `Unknown opcode 0 for object
<anonymous>@0`
([ZED-9KB](https://zed-dev.sentry.io/issues/7568720776/)). When the
export request fails because the surface is already dead,
wayland-scanner's generated code silently returns an inert proxy, and
ashpd's `Drop` impl later sends `destroy` on it — which wayland-backend
0.3.11 answers with a panic, because it looks up the request opcode
before checking whether the object is null. wayland-backend 0.3.15 fixes
this
([Smithay/wayland-rs#890](https://github.com/Smithay/wayland-rs/issues/890))
by returning an error instead, which the generated destructor discards,
so the drop becomes a harmless no-op. This bumps the lockfile to 0.3.15
and raises the version floor in `gpui_linux` so the fix can't silently
regress via a fresh lockfile.

Closes FR-100

Release Notes:

- Fixed a crash on Linux (Wayland) when a window was closed just as a
file dialog was being opened.
2026-07-08 20:37:57 +00:00
Philipp Schaffrath
664b7ecb40
gpui: Fix hover state not clearing when mouse leaves window (#60275)
# Objective

Fix two gaps in element hover tracking at window boundaries. Hover was
only re-evaluated on `MouseMove`, so when the pointer left the window no
event fired `on_hover(false)` and the element stayed hovered.
Symmetrically on Wayland, no `Motion` follows `Enter` until the pointer
moves again, so hover was not established at the entry pixel. Both cases
are easy to miss since most hover-styled elements don't sit flush
against the window edge, but they surfaced while implementing
layer_shell popups with input_regions, which should close when stop
hovering.

## Solution

The hover compare-and-fire logic in `div` is refactored into a shared
`update_hover` closure, and a second listener on `MouseExitEvent` clears
hover when the pointer leaves the window. It clears unconditionally
because `MouseExited` doesn't update the tracked mouse position, so a
hit test during that dispatch would still report the element as hovered.

On Wayland, a `MouseMove` is synthesized at the entry position on
`wl_pointer.enter`, mirroring the `MouseExited` already dispatched on
`Leave`.

## Testing

Tested manually on Wayland/Linux: hover on a window-edge element clears
when the pointer leaves the window, and hover is established immediately
when the pointer enters a surface with an element under the entry pixel.

Not tested on other platforms. The `div` change relies on each
platform's existing `MouseExited` dispatch: macOS and X11 emit it, so
they get the exit fix too. Windows never dispatches `MouseExited`
(`WM_MOUSELEAVE` only flips the window-level hover flag), so the
stuck-hover case might remain there, unchanged from before.

Before:


https://github.com/user-attachments/assets/6af83bb3-de9d-40e2-a64c-bdefc98fc96d

After:


https://github.com/user-attachments/assets/721a9b5c-108a-499f-867e-da111836a34a


Here is an example application to test this:

```rs
#![cfg_attr(target_family = "wasm", no_main)]

use gpui::{
    App, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, rgb, size,
};
use gpui_platform::application;

struct HoverExit {
    hovered: bool,
}

impl Render for HoverExit {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        // Fills the whole window so its edge is the window edge: moving the mouse
        // out of the window is what exercises the MouseExited path.
        div()
            .id("hover-exit")
            .size_full()
            .flex()
            .justify_center()
            .items_center()
            .text_xl()
            .text_color(rgb(0xffffff))
            .bg(if self.hovered {
                rgb(0x585f58)
            } else {
                rgb(0x505050)
            })
            .child(if self.hovered { "HOVERED" } else { "not hovered" })
            .on_hover(cx.listener(|this, hovered, _, cx| {
                this.hovered = *hovered;
                cx.notify();
            }))
    }
}

fn run_example() {
    application().run(|cx: &mut App| {
        let bounds = Bounds::centered(None, size(px(240.), px(160.0)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                app_id: Some("gpui-hover-exit".to_string()),
                ..Default::default()
            },
            |_, cx| cx.new(|_| HoverExit { hovered: false }),
        )
        .unwrap();
        cx.activate(true);
    });
}

#[cfg(not(target_family = "wasm"))]
fn main() {
    run_example();
}

#[cfg(target_family = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn start() {
    gpui_platform::web_init();
    run_example();
}
```


## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed element hover state not clearing when the mouse leaves the
window
2026-07-08 20:27:47 +00:00
Richard Feldman
23c0080d1d
gpui_windows: Check composition attribute lookup (#60122)
Checks that SetWindowCompositionAttribute was found before transmuting
and calling the function pointer, making the composition setup a no-op
when the undocumented export is unavailable.

Release Notes:

- N/A
2026-07-08 19:27:01 +00:00
Philipp Schaffrath
546a16d64f
gpui: Add parent-anchored native popup windows (with wayland xdg_popup implementation only so far) (#60232)
# Objective

gpui can't show UI that extends past the window it belongs to. Menus,
dropdowns and tooltips are drawn as elements inside the window, so they
clip at its edges. This PR adds a window kind for platform-native popups
anchored to a parent window, as groundwork for real native menus,
dropdowns and tooltips.

## Solution

`WindowKind::AnchoredPopup(PopupOptions)` opens a popup positioned
relative to a parent window. Instead of giving the popup an absolute
position, you describe where it should go and the platform figures out
the rest:

- `parent`: the window to anchor to
- `anchor_rect`: a rectangle in the parent, e.g. the button that opened
the menu
- `anchor` and `gravity`: which point of that rect to attach to, and
which direction to grow
- `constraint_adjustment`: what the platform may do if the popup would
leave the screen (slide, flip, resize)
- `grab`: menu behavior, the popup takes focus and is dismissed when
clicking outside the app

The popup's size comes from `WindowOptions::window_bounds`.

This model mirrors Wayland's `xdg_positioner`, where the compositor owns
positioning and the client can only describe intent. Since that's the
most restrictive case, the other platforms can implement the same
description later with simple math against screen bounds.

Only Wayland is implemented so far, via `xdg_popup` on top of the
existing surface implementation. Popups can be parented to toplevels,
layer-shell surfaces (a menu opened from a panel) and other popups
(nested menus). macOS, Windows, X11 and web reject the kind with
`PopupNotSupportedError`, so callers can detect that and fall back to
in-window popovers.

Some Wayland details that might help during review:

- Anchor rects are translated from gpui coordinates into the parent's
window geometry space and clamped to it. A rect outside the geometry, or
with zero size, is a fatal protocol error
- Resizing a mapped popup goes through `xdg_popup.reposition`
- Mouse press serials are now recorded on press only, not release.
Compositors decline grabs and interactive moves that reference a release
serial

## Testing

Tested manually on Wayland with an example app: the menu opens anchored
below its button, extends past the parent window, flips above the button
near the bottom of the screen, and a grabbing popup is dismissed when
clicking into another application.

Nested menus were tested in one of my projects (ignore that they are
ugly, that's just a prototype 😛):


https://github.com/user-attachments/assets/2cd3e2e9-87f7-4b02-986f-48e5633e205c




I also have a complete runnable example demonstrating it. I did not add
it to the PR, because this might give the impression that
`WindowKind::AnchoredPopup` are a complete implementation, despite only
working on wayland so far:

<details>
  <summary>Click to view example</summary>

```rust
//! Example and manual test for platform-native popups (`WindowKind::AnchoredPopup`).
//!
//! A native popup is a real, parent-anchored window that can extend beyond its parent onto the
//! screen, unlike gpui's in-window popovers. Run it, open the menu, and confirm the points listed
//! in the window. On a platform without an implementation the button reports that popups are not
//! supported instead of opening anything.
//!
//! Run with: cargo run -p gpui --example popup

#![cfg_attr(target_family = "wasm", no_main)]

use gpui::{
    AnyWindowHandle, App, Bounds, Context, MouseButton, SharedString, Window, WindowBounds,
    WindowHandle, WindowKind, WindowOptions, div, point, popup::*, prelude::*, px, rgb, size,
};
use gpui_platform::application;

/// The trigger button, at a fixed position so the popup can anchor to a known rectangle. Real code
/// would anchor to the measured bounds of whatever element opens the popup.
const BUTTON_BOUNDS: Bounds<gpui::Pixels> = Bounds {
    origin: point(px(24.), px(24.)),
    size: size(px(200.), px(32.)),
};

const POPUP_SIZE: gpui::Size<gpui::Pixels> = size(px(260.), px(320.));

struct Menu;

impl Render for Menu {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        let item = |label: &str| {
            div()
                .id(label.to_string())
                .px_3()
                .py_1()
                .rounded_sm()
                .hover(|this| this.bg(rgb(0x3a3a3a)))
                .cursor_pointer()
                .child(label.to_string())
                .on_click(|_, window, _| window.remove_window())
        };

        div()
            .id("menu-root")
            .size_full()
            .p_1()
            .flex()
            .flex_col()
            .gap_0p5()
            .bg(rgb(0x2a2a2a))
            .text_color(gpui::white())
            .rounded_md()
            .border_1()
            .border_color(rgb(0x454545))
            .child(item("Foo"))
            .child(item("Bar"))
            .child(item("Baz"))
            .child(item("Qux"))
            .child(item("Alice"))
            .child(item("Bob"))
    }
}

struct PopupExample {
    menu: Option<WindowHandle<Menu>>,
    status: SharedString,
}

impl Default for PopupExample {
    fn default() -> Self {
        Self {
            menu: None,
            status: "Click \"Open menu\" to open a native popup.".into(),
        }
    }
}

impl PopupExample {
    /// Closes the menu if it is open. Returns true if a menu was actually open.
    fn close_menu(&mut self, cx: &mut App) -> bool {
        match self.menu.take() {
            Some(menu) => menu
                .update(cx, |_, window, _| window.remove_window())
                .is_ok(),
            None => false,
        }
    }

    fn toggle_menu(&mut self, parent: AnyWindowHandle, cx: &mut App) {
        if self.close_menu(cx) {
            return;
        }
        match open_menu(parent, cx) {
            Ok(menu) => {
                self.menu = Some(menu);
                self.status = "Menu open. Dismiss it by selecting an item, clicking elsewhere in \
                    this window, or clicking another application."
                    .into();
            }
            // A real application would fall back to an in-window popover here.
            Err(error) => {
                self.status = format!("Failed to open a native popup: {error}").into();
                log::error!("failed to open popup: {error}");
            }
        }
    }
}

impl Render for PopupExample {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let bullet = |text: &str| div().child(format!("• {text}"));

        div()
            .id("root")
            .size_full()
            .bg(rgb(0xf7f7f7))
            .text_color(rgb(0x222222))
            // Same-app clicks don't auto-dismiss a grabbing popup (see `PopupOptions::grab`).
            .on_mouse_down(
                MouseButton::Left,
                cx.listener(|this, _, _window, cx| {
                    this.close_menu(cx);
                }),
            )
            .child(
                div()
                    .size_full()
                    .p_5()
                    .pt(px(76.))
                    .flex()
                    .flex_col()
                    .gap_3()
                    .child(div().text_xl().child("Native popup test"))
                    .child(div().text_sm().child(
                        "WindowKind::AnchoredPopup opens a real, parent-anchored window that can \
                         extend past this window onto the screen. Only some platforms implement \
                         it so far.",
                    ))
                    .child(
                        div()
                            .flex()
                            .flex_col()
                            .gap_1()
                            .text_sm()
                            .text_color(rgb(0x555555))
                            .child(div().child("Verify:"))
                            .child(bullet("The menu opens anchored below the button."))
                            .child(bullet(
                                "The menu extends past the bottom edge of this window.",
                            ))
                            .child(bullet(
                                "Near the bottom of the screen, the menu flips above the button.",
                            ))
                            .child(bullet("Clicking another application dismisses the menu."))
                            .child(bullet(
                                "Selecting an item or clicking in this window dismisses it.",
                            )),
                    )
                    .child(
                        div()
                            .text_sm()
                            .text_color(rgb(0x333333))
                            .child(self.status.clone()),
                    ),
            )
            .child(
                div()
                    .absolute()
                    .left(BUTTON_BOUNDS.origin.x)
                    .top(BUTTON_BOUNDS.origin.y)
                    .w(BUTTON_BOUNDS.size.width)
                    .h(BUTTON_BOUNDS.size.height)
                    .flex()
                    .items_center()
                    .justify_center()
                    .bg(rgb(0xffffff))
                    .border_1()
                    .border_color(rgb(0xd0d0d0))
                    .rounded_md()
                    .cursor_pointer()
                    .id("open-menu")
                    .active(|this| this.bg(rgb(0xeeeeee)))
                    .child("Open menu ▾")
                    // Open on mouse-down, not on click, so the grab is taken while the button is still held.
                    .on_mouse_down(
                        MouseButton::Left,
                        cx.listener(|this, _, window, cx| {
                            // Don't let the window handler above close the menu we are opening.
                            cx.stop_propagation();
                            this.toggle_menu(window.window_handle(), cx);
                        }),
                    ),
            )
    }
}

fn open_menu(parent: AnyWindowHandle, cx: &mut App) -> anyhow::Result<WindowHandle<Menu>> {
    cx.open_window(
        WindowOptions {
            titlebar: None,
            // Sizes the popup. The platform decides the position, so the origin is ignored.
            window_bounds: Some(WindowBounds::Windowed(Bounds {
                origin: point(px(0.), px(0.)),
                size: POPUP_SIZE,
            })),
            kind: WindowKind::AnchoredPopup(PopupOptions {
                parent,
                anchor_rect: BUTTON_BOUNDS,
                // Anchor to the button's bottom-left and grow down-right so the menu drops beneath it.
                anchor: PopupAnchor::BottomLeft,
                gravity: PopupGravity::BottomRight,
                // Slide horizontally and flip vertically if the menu would leave the screen.
                constraint_adjustment: PopupConstraintAdjustment::SLIDE_X
                    | PopupConstraintAdjustment::FLIP_Y,
                offset: point(px(0.), px(4.)),
                // Grab input so the compositor dismisses the popup on clicks into other applications.
                grab: true,
            }),
            ..Default::default()
        },
        |_, cx| cx.new(|_| Menu),
    )
}

fn run_example() {
    application().run(|cx: &mut App| {
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(Bounds {
                    origin: point(px(100.), px(100.)),
                    size: size(px(420.), px(300.)),
                })),
                ..Default::default()
            },
            |_, cx| cx.new(|_| PopupExample::default()),
        )
        .unwrap();
        cx.activate(true);
    });
}

#[cfg(not(target_family = "wasm"))]
fn main() {
    run_example();
}

#[cfg(target_family = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn start() {
    gpui_platform::web_init();
    run_example();
}
```

</details>

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- N/A
2026-07-08 19:22:23 +00:00
Keith Hall
6b733d1058
search: Bump fancy-regex dependency and enable CRLF mode (#55471)
Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #43396

Release Notes:

- Project search now supports CRLF line endings correctly, as well as
other regex features like subroutine calls
2026-07-08 16:56:45 +00:00
G36maid
b9f3396b68
Add support for format_on_save only changed lines (#49964)
## Summary

Introduces `format_on_save` variants that format only modified lines,
limiting formatting to lines changed since the last commit. This
prevents massive diffs when editing legacy codebases. Aligns with VS
Code's `editor.formatOnSaveMode` naming:

- `"modifications"` — formats only git-diffed lines. Requires source
control; skips formatting if no diff is available.
- `"modifications_if_available"` — formats only git-diffed lines,
falling back to full-file formatting for untracked files, when source
control is unavailable, or when the LSP does not support range
formatting.

Also supports importing equivalent VS Code settings
(`formatOnSaveMode`).

This PR uses the range-based whitespace/newline infrastructure from the
dependency PR above.

## Changes


<details>
<summary><b>New settings, modified ranges computation, VS Code
import</b></summary>

### New settings (`language.rs`, `default.json`, `all-settings.md`)

Two new `FormatOnSave` variants: `Modifications` and
`ModificationsIfAvailable`.

### Modified ranges computation (`items.rs`)

- `compute_format_decision()` — reads `format_on_save` setting across
all buffers, determines whether to use ranged formatting (`Ranges`),
full formatting (`Full`), or skip (`Skip`).
- `compute_modified_ranges()` — extracts modified line ranges from git
diff hunks via `BufferDiffSnapshot`.
- `is_empty_range()` — helper to detect deletion-only hunks that produce
no formatable content.

The `save()` method calls `compute_format_decision()` and dispatches
accordingly.

### Modifications mode in `lsp_store.rs`

- Adds `FormatOnSave::Modifications |
FormatOnSave::ModificationsIfAvailable` to the formatter selection match
arm.
- `ModificationsIfAvailable` falls back to full-file formatting when
ranged formatting produces no results.

### VS Code settings import (`vscode_import.rs`, `settings_store.rs`)

Imports `editor.formatOnSaveMode` mapping:
- `"modifications"` → `FormatOnSave::Modifications`
- `"modificationsIfAvailable"` →
`FormatOnSave::ModificationsIfAvailable`
- `"file"` → `FormatOnSave::On`

</details>

## Tests

- `test_modifications_format_on_save` — basic modifications mode
formatting with dirty buffer
- `test_modifications_format_no_changes` — clean buffer triggers no
formatting
- `test_modifications_format_lsp_no_range_support` — LSP without range
formatting skips entirely for `Modifications`
- `test_modifications_format_lsp_returns_empty_edits` — empty edits
handled gracefully
- `test_modifications_format_multiple_hunks` — two non-adjacent edits
produce two separate range formatting requests

---

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable


Closes #16509
Depends on #53942

Release Notes:

- Added `modifications` and `modifications_if_available` options to
`format_on_save`, which format only git-changed lines instead of the
entire file
- When using modifications mode, `remove_trailing_whitespace_on_save`
and `ensure_final_newline_on_save` are also scoped to changed lines,
preventing unwanted diff jitter in legacy codebases
- Added support for importing VS Code's `editor.formatOnSaveMode`
setting

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-08 16:53:11 +00:00
Lena
7dc634124c
Switch Guild board automation to role checks (#60606)
Outside collaborators can't be added to GitHub teams, team membership is
restricted to org members.

https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-outside-collaborators/adding-outside-collaborators-to-repositories-in-your-organization#:~:text=Outside%20collaborators%20cannot%20be%20added%20to%20a%20team%2C%20team%20membership%20is%20restricted%20to%20members%20of%20the%20organization

Release Notes:

- N/A
2026-07-08 16:52:33 +00:00
TwoClocks
029bf2f284
Make editor::LineUp & editor::LineDown honor vertical_scroll_margin like vim::LineUp & vim::LineDown (#52057)
## Context

This is an implementation of this feature:
https://github.com/zed-industries/zed/discussions/49821

Although, I'd argue it's a bug fix, not a feature.

Either way : This copies the window scroll logic from the `vim` mode
versions with out all the extra logic for visual mode.

Could refactor the common logic out of the vim code and make it common.
But that seems like a bigger PR. Happy to take a stab at it if that's
what you prefer.
Could also add new commands for the new behavior if you prefer. I didn't
do that, because it seems like more clutter in the commands, and my
belief that the existing behavior is a bug. But happy to do that if you
prefer.

## How to Review

creates new function `scroll_screen_with_cursor_margin` in `scroll.rs`
wires up editor::LineUp/LineDown to new function in `elements.rs`
adds a test in  `editor_tests.rs`

## Self-Review Checklist

<!-- Check before requesting review: -->
- [X] I've reviewed my own diff for quality, security, and reliability
- [X] Unsafe blocks (if any) have justifying comments
- [X] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [X] Tests cover the new/changed behavior
- [X] Performance impact has been considered and is acceptable

Release Notes:

- editor::LineUp/LineDown commands now honor vertical_scroll_margin
(same as vim::LineUp/LineDown)

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2026-07-08 16:37:08 +00:00
Jiby Jose
7f5cf583dc
Fix worktree entry IDs for symlinked files (#57846)
Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #55792

Release Notes:

- Fixed files in pnpm workspaces moving to symlinked `node_modules`
paths after saving.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-08 16:37:02 +00:00
zed-zippy[bot]
6979d7281f
Bump Zed to v1.12.0 (#60600)
Release Notes:

- N/A

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
2026-07-08 16:06:44 +00:00
Oleksandr Kholiavko
d9e35975c2
csv_preview: Add row filtering feature (#60339)
# Objective

CSV feature needs row filtering feature by column values. This PR
provides base implementation of it with barebones ui.

> NOTE: Sleek UI with search & proper scrolling hanling is implemented
in next PR. It's stacked on top to reduce review scope

## Solution

- New `FilterEntry` / `FilterEntryState` model in
`table_data_engine/filtering_by_column.rs` tracking per-column
applied/candidate filter values
- Filtering runs in the background (`feat: Implement background
filtering`) so large CSVs don't block the UI thread while a filter is
applied
- Filter menu entries reflect live counts and support a configurable
sort order (`FilterSortOrder`, added in `renderer/settings.rs` /
`settings.rs`)
- Filter/sort trigger buttons on column headers are hidden until hover,
using `GradientFade` (new in `ui/src/components/gradient_fade.rs`) to
fade content behind them

## Testing

Filter chain tested on csv fixtures with multiple filters applied
sequentially columns.

## Self-Review Checklist: (todo)

- [ ] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [ ] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines) (out of scope of this pr)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

<img width="664" height="249" alt="image"
src="https://github.com/user-attachments/assets/0e9b0a91-1a27-4e0f-a8d4-fdce36735131"
/>

<img width="663" height="205" alt="image"
src="https://github.com/user-attachments/assets/0428f5c6-6aaa-4891-b010-ca79803f6613"
/>

---

Release Notes:

- Added initial row filtering UI & logic
2026-07-08 15:54:36 +00:00
Mikayla Maki
74b5207744
Unify Render and RenderOnce into View (#58087)
This allows us to build powerful and flexible Input and TextArea
components

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 15:52:48 +00:00
soddygo
c80020231b
agent_ui: Fix expanded message editor staying auto-height during streaming (#55916)
Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments (Unsafe: none)
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable


## Summary
When the agent is streaming output, expanding the message editor can
appear to work (the container grows) but the input editor itself remains
constrained to auto-height (only a few lines). Toggling minimize →
expand makes it render correctly.

This PR ensures the message editor stays in full mode while expanded,
preventing automatic editor mode syncing on thread updates from
overriding the user's explicit expand action during streaming.

## Steps to reproduce
1. Start an agent thread and send a message that causes streaming output
2. While streaming, click “Expand Message Editor”
3. Observe the input editor still shows only a few lines (auto-height)
4. Click “Minimize Message Editor” and then expand again; it becomes
fully expanded

## Test plan
- Start an agent thread and let it stream
- Click “Expand Message Editor” while streaming
- Verify the editor actually expands (not limited to the auto-height max
lines)
- Toggle minimize/expand multiple times during streaming; verify it
remains correct

## Additional context
Repro video: 


https://github.com/user-attachments/assets/6e328fa8-d431-456a-b70a-864ae617ea88



Release Notes:

- Fixed message editor not fully expanding during agent generation

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-08 15:52:26 +00:00
Mikayla Maki
74798c68d5
gpui: Add run_embedded and ApplicationHandle for externally driven run loops (#60574)
On ordinary platforms, `Platform::run` blocks for the lifetime of the
app, and `Application::run`'s stack frame keeps the app state alive.
Embedded platforms invert that: the run loop belongs to someone else.

`Application::run_embedded` supports that shape: it starts the app
exactly like `run()`, but returns an `ApplicationHandle` holding the
strong app handle.

Release Notes:

- N/A
2026-07-08 15:46:41 +00:00
Neel
86b2831c7c
editor: Use raw head selection for rename (#60594)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Follow up to https://github.com/zed-industries/zed/pull/55542, which
reused the shifted offset for the rename input's *initial selection*,
cutting it one character short.

<img width="524" height="512" alt="image"
src="https://github.com/user-attachments/assets/37032614-2cfd-4ad4-9005-f2eac57924a1"
/>
<img width="435" height="158" alt="image"
src="https://github.com/user-attachments/assets/7b1b9c52-08d6-48ce-928a-873f6e175f65"
/>

---

Release Notes:

- Fixed symbol rename in vim mode omitting the last character of the
symbol out of the rename input's initial selection
2026-07-08 14:19:58 +00:00
Patryk
05530c9b35
acp_thread: Log error when checkpoint comparison fails (#59196)
`update_last_checkpoint` swallows `compare_checkpoints` errors with
`.unwrap_or(true)`. The "Restore checkpoint" button silently disappears
and nothing gets logged, which is what made the linked issue painful to
track down in the first place.

The sibling `update_last_checkpoint_if_changed` a few lines up already
handles the same call with `.context(...).log_err()` and an early
return, so I did the same here. On error the checkpoint's visibility is
left alone instead of being forced to hidden. I didn't propagate the
error because the task result gets `?`'d in `run_turn`'s cleanup, and
failing there would leave the panel stuck in its generating state.

Added a regression test that breaks the comparison mid-turn (recreating
`.git` makes the fake repo forget its checkpoints) and asserts an
already-visible checkpoint stays visible. It fails on main and passes
with this change. The new log line shows up when it runs: `failed to
compare checkpoints: invalid left checkpoint: ...`

Closes #59100

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed checkpoint comparison errors silently hiding the "Restore
Checkpoint" button in the agent panel.

Co-authored-by: pstemporowski <110726755+pstemporowski@users.noreply.github.com>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2026-07-08 13:44:41 +00:00
Vlad Roskov
a3a4719a8a
project_panel: Open files in a permanent tab on middle click (#60563)
# Objective

Provide a means of quickly opening a permanent tab from project panel
instead of a preview tab.

Implements #51866 (_Middle click to open file in a new tab_ with 18×↑)
which is also part of #31822 (_Middle Click Improvments_) titled "middle
clicking on a file in the project panel should open the file in a
non-transient state".

With `preview_tabs.enable_preview_from_project_panel` enabled, a click
in the project panel opens a preview tab, and there's no easy gesture to
open a permanent one instead, simplest one currently being a double
click. VS Code and VSCode-based editors recognize middle mouse click as
a way to open a permanent tab since 2016 (microsoft/vscode#14453).

## Solution

Wired up an `on_aux_click` handler for project panel entries:
middle-clicking a file opens it in a permanent tab and focuses it,
regardless of the preview tabs setting.

Added a line to `docs/src/project-panel.md` to reflect the behavior.

## Testing

- Manually tested on Windows: middle click opens a permanent tab,
promotes an existing preview tab to a permanent one if already open,
does nothing for directories
- Haven't tested on macOS/Linux

## Self-Review Checklist:

- [ ] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

<details>
  <summary>Click to view showcase</summary>


https://github.com/user-attachments/assets/2de5f23d-637f-4ffc-8d03-02dc11714af4

</details>

---

Release Notes:

- Added support for middle-clicking a file in the project panel to open
it in a permanent tab instead of a preview tab
2026-07-08 13:06:10 +00:00
Tianze Zhao
f5c975162c
terminal: Open links with Cmd/Ctrl-click when mouse mode is enabled (#60067)
This fixes #56956.

The terminal link-open gesture was split between `mouse_down` and
`mouse_up`, but both halves only ran in the non-mouse-reporting path.
When a foreground app enabled terminal mouse reporting, a secondary
click on a URL or file path was forwarded to the PTY instead of opening
the link.

This changes secondary-click link handling so it checks for a hyperlink
before mouse reporting on press, and completes the matching hyperlink
open before mouse reporting on release. The event is only consumed when
the click actually lands on the same link, so normal clicks in
mouse-reporting TUIs continue to be forwarded as before.

Validation:

- `CARGO_BUILD_JOBS=1 cargo test -j1 -p terminal
test_hyperlink_ctrl_click_same_position_in_mouse_mode`
- `CARGO_BUILD_JOBS=1 cargo check -j1 -p terminal`

Release Notes:

- Improved terminal links: Cmd/Ctrl-click now opens links even when the
application has mouse reporting enabled (e.g. vim, opencode, claude).
Disable `terminal.open_links_in_mouse_mode` to forward these clicks to
the application instead.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-08 12:41:48 +00:00
Michael Egger
739f23926d
grammars: Recognize Gentoo ebuild files as bash script (#59068)
The Gentoo `ebuild` file format is a subset of a bash script, see
https://wiki.gentoo.org/wiki/Ebuild.


Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #ISSUE

Release Notes:

- Changed `ebuild` files to be recognized as bash.

Signed-off-by: gcarq <egger.m@protonmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-08 12:36:29 +00:00
Miguel Raz Guzmán Macedo
ded93ccb08
Fix typos and grammatical mistakes in docs (#59495)
# Objective

- Describe the objective or issue this PR addresses. -> ** Typo and
grammar fixes!**

## Solution


## Testing

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

---

Release Notes:

- N/A
2026-07-08 12:32:00 +00:00
Guilherme Tavares
385e9c68ae
Suggest language extensions for untitled buffers (#55263)
Fixes #53527.

## Summary

- Suggest `untitled.<extension>` when saving an untitled editor buffer
with a selected non-Plain Text language.
- Preserve the existing title-based suggestion for existing files, Plain
Text buffers, and buffers without a language extension.
- Add a regression test for an untitled Rust buffer suggesting
`untitled.rs`.

## Testing

- `mise exec rust@1.95.0 -- cargo fmt --check -p editor`
- `mise exec rust@1.95.0 -- cargo test -p editor
test_suggested_filename_uses_language_extension_for_untitled_buffer
--lib`

## Suggested .rules additions

None.

Release Notes:

- Fixed Save As suggestions for untitled buffers with a selected
language.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-08 12:29:04 +00:00
olegator888
35ddcb2ecd
go: Fix outline for methods with unnamed receivers (#58656)
Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

This fixes Go outline extraction for methods whose receiver has no name,
such as `func (v2) Method()`. These methods are now included in outline
symbols, which also feed breadcrumbs and sticky
scroll.

Tested with:

- `cargo test -p languages`
- `cargo test -p grammars`
- `./script/clippy -p languages`
- `cargo fmt --check --package languages`

Before:

[before_cut.webm](https://github.com/user-attachments/assets/91eb5cb0-703a-4496-b0dd-5369c4c219fc)

After:

[after_cut.webm](https://github.com/user-attachments/assets/76d13d88-3671-4118-99fc-c073a6e64727)

Release Notes:

- Fixed Go methods with unnamed receivers not appearing in breadcrumbs
and sticky scroll.

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-08 12:21:46 +00:00
Ibrahim Khan
2243c13b9b
project: Fix content swap when an LSP rename also renames the file (#59104)
A "rename symbol" whose workspace edit also renames the file (a
`TextDocumentEdit` followed by a `RenameFile` resource operation) only
applied the text edit to the in-memory buffer. The on-disk file still
held the pre-edit content, so the blind `fs.rename` moved stale bytes to
the new path while the edited buffer was stranded at the old path,
swapping the two files' contents.

This persists a dirty buffer for the rename source before renaming, so
the new file receives the edited content and the now-clean buffer can't
be saved back to the old path.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #59077

Release Notes:

- Fixed a symbol rename that also renames the file swapping the contents
of the old and new files
2026-07-08 12:21:10 +00:00
Tom Houlé
2ab35c6b7d
Consistently use context() to preserve sources for anyhow errors (#59112)
When you use `anyhow::anyhow!("{error}")` to convert a preexisting error
to an `anyhow::Error`, the error source can be lost (depending on the
`Display` impl of the error). Anyhow errors can display the whole source
chain when printed. This commit makes us consistently use `context()`
instead to preserve the underlying error's source.

Release Notes:

- N/A
2026-07-08 12:20:52 +00:00
Miguel Raz Guzmán Macedo
bc3c9422f4
Use more .array_windows::<N>() (#58877)
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

The fight against entropy continues...

Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-08 12:10:32 +00:00
Rain
01235732e6
Fix should_log_lsp_request_failure logic inversion (#57554)
Was trying to debug an r-a issue and ran into this.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliabilityx
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #ISSUE

Release Notes:

- N/A or Added/Fixed/Improved ...

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-08 12:08:36 +00:00
liam
d4dfe87ce0
workspace: Skip closed items that cannot be reopened (#56299)
Resolves https://github.com/zed-industries/zed/issues/55600

This diff fixes `pane::ReopenClosedItem` getting stuck when the
closed-item stack contains entries that cannot be reopened, such as
Project Search, untitled buffers, or Default Settings. Previously,
`Workspace::navigate_history_impl` would pop the newest closed entry and
stop if that item was no longer present in the pane and had no path
recorded for reopening. That made `cmd- shift-t` appear to do nothing
until enough attempts had consumed those unreopenable entries.

With this change, closed-item navigation keeps scanning when it
encounters an entry that cannot be activated or reopened by path. This
preserves the current path-based reopening behavior for normal files,
while avoiding no-op shortcuts caused by non-file items in the closed
stack.

This made me wonder whether or not we'd eventually want full reopen
support for non-traditional items like Project Search or bundled
settings editors. Supporting that properly would require storing
item-specific restoration state, such as search query/options for
Project Search or a bundled-file descriptor for Default Settings, and
teaching closed-item navigation how to recreate those items from that
state. Something definitely out of scope for this PR.

| Before | After |
| --- | --- |
| <video
src="https://github.com/user-attachments/assets/c7044423-4531-4857-84f2-4e9651826c6a"
controls width="500" title="Before"></video> | <video
src="https://github.com/user-attachments/assets/c89dcefb-1796-4cdf-bb21-f165145e678e"
controls width="500" title="After"></video> |

Release Notes:
- Fixed reopening closed tabs getting stuck on closed items that cannot
be reopened.
2026-07-08 11:57:02 +00:00
Matt Good
950ec7943f
editor: Decode escaped characters in hover popover links (#55973)
Decodes url escape sequences in hover preview `file:///` links like
escaped
spaces in the file path.

I'm working on an LSP and happened to be working with some files in a
directory with spaces. When adding Markdown links with `file:///` the
`%20` escape for spaces was being included verbatim in the path that Zed
tried to open.

I'm reusing the lines from `markdown_preview_view.rs` for decoding. In
the existing tests I don't see coverage for `file:///` links. If you'd
like some tests for this can you point me to any examples to start from?

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed decoding spaces and other escaped characters in `file://` links
used in hover popovers

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
2026-07-08 09:01:58 +00:00
Marshall Bowers
f9c994796a
cloud_api_client: Make send_authenticated_json_request public (#60562)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
This PR makes the `send_authenticated_json_request` method on the
`CloudApiClient` public.

This way we can use it to make requests by external callers.

The `build_request` method was also inlined into
`send_authenticated_request` to make the contract simpler.

Release Notes:

- N/A
2026-07-07 23:00:12 +00:00
Anant Goel
d12b980ee0
bedrock: Add native support for Bedrock Mantle models (#60480)
Adds native support for AWS Bedrock's Mantle endpoint
(`bedrock-mantle`), which serves models with no `Converse`/`Invoke`
support on `bedrock-runtime`, such as GPT-5.5, GPT-5.4, and Grok 4.3 but
more importantly **open-weight** models

Closes #60471 

## What's changed

- Renamed the existing `Model` enum in the `bedrock` crate to
`ConverseModel`, and added a new `MantleModel` enum for Mantle-only
models. Mantle models reuse the existing OpenAI-compatible Chat
Completions/Responses request and response plumbing
(`into_open_ai`/`into_open_ai_response`,
`OpenAiEventMapper`/`OpenAiResponseEventMapper`) already used by the
native OpenAI and OpenAI-compatible providers, rather than introducing
new marshalling code.
- Added a `BedrockMantleModel` language model that routes requests to
the `bedrock-mantle` endpoint, dispatching to Chat Completions or the
Responses API depending on the model. Mantle models appear in the model
picker alongside Converse models under the same Bedrock provider.
- Added region gating: `bedrock-mantle` is only available in a subset of
AWS Regions, so using a Mantle model outside of them surfaces a clear
error naming the current Region and the supported ones, instead of an
opaque HTTP failure.
- Implemented Bedrock bearer token authentication for Mantle requests: a
configured Bedrock API key is used as-is, and every other auth method
(IAM credentials, named profile, SSO, automatic) derives a short-term
token by locally SigV4-presigning a `CallWithBearerToken` request. This
requires no extra network round trip and no token caching, since
re-signing locally is cheap.
- Added a specific error for the 403 you get when your credentials have
`bedrock:CallWithBearerToken` but not the separate
`bedrock-mantle:CallWithBearerToken` permission Mantle models require,
since this is the most common misconfiguration.
- Added a `mantle_available_models` setting so custom models served
through `bedrock-mantle` can be configured, the same way other providers
support custom models via `available_models`.
- Documented Mantle models and the new setting in the Amazon Bedrock
section of [Use a
Gateway](https://zed.dev/docs/ai/use-a-gateway#amazon-bedrock).

## Testing

- Added unit tests covering: the local SigV4 bearer-token signing
(including a byte-for-byte cross-check against a reference
implementation), Mantle endpoint URL construction, the
Mantle-supported-regions list, thinking-effort normalization, and the
settings-to-model protocol mapping.
- `cargo test -p bedrock -p language_models -p settings_content -p
settings` passes.
- `./script/clippy` passes with no new warnings.

Release Notes:

- Added native support for AWS Bedrock's Mantle endpoint, enabling
GPT-5.5, GPT-5.4, and Grok 4.3 through the Amazon Bedrock provider.
2026-07-07 22:20:34 +00:00
Ben Kunkle
5d7d1d3f09
language_extension: Hold LspStore weakly in LspAccess::ViaLspStore (#60558)
# Objective

Fix a Windows CI flake in `extension_host
extension_store_test::test_extension_store_with_test_extension`, which
panicked in GPUI's leak detector with three leaked `LspStore` handles
([example
run](https://github.com/zed-industries/zed/actions/runs/28871529605/job/85635418351)).

## Solution

`LspAccess::ViaLspStore` held a strong `Entity<LspStore>`, cloned into
three `ExtensionHostProxy` registrations by `language_extension::init`.
The proxy sits inside an `Arc` cycle (proxy →
`LanguageServerRegistryProxy` → `LanguageRegistry` →
`ExtensionLspAdapter` → `WasmExtension` → `WasmHost` → proxy), so
whenever an extension LSP adapter was registered at app-drop time, the
cycle pinned the `LspStore` entity after its owning `Project` dropped.
The flake was purely timing: extension reload toggles the adapter
registration, and an unclean LSP pipe shutdown on Windows shifted
teardown into the pinned window.

`ViaLspStore` now holds a `WeakEntity<LspStore>`, upgraded at its sole
use site (`remove_language_server`), skipping the stop task when the
store is gone — matching the existing `ViaWorkspaces` semantics. A dead
store is the expected terminal state after the owning project drops, so
no error is logged or propagated. `Project`/`HeadlessProject` remain the
sole long-term owners of `LspStore`, which is already the convention
everywhere else (e.g. `json_schema_store`, the `lsp_store` message
handlers).

## Testing

- Ran `cargo nextest run -p extension_host
extension_store_test::test_extension_store_with_test_extension` locally:
passes.
- The flake is timing-dependent (reproduced on Windows CI), so a local
pass doesn't prove absence; the fix removes the only strong non-owner
handle, which the leak detector reported.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- N/A

---------

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
2026-07-07 21:59:03 +00:00
Matt Van Horn
8d932b0e06
git_ui: Use pull request link from push output in push toast (#60522)
## Summary

Fixes #60288 and #60454 .

After a push, the push toast's "Create Pull Request" button fails with
`Unsupported remote URL` when the repository's remote is not a plain,
recognized host. This restores the pre-#53913 behavior as a fallback:
use the create-PR/MR link that `git push` itself prints, and only build
a URL from the provider registry when the push output had no link.

## Why this matters

#53913 made the button always appear and changed `create_pull_request`
to reconstruct the PR URL from the remote via
`git::parse_git_remote_url` against the `GitHostingProviderRegistry`.
When the remote is not recognized, parsing returns `None` and the action
errors. This affects self-hosted GitLab/GitHub, an SSH-config host alias
like `git@personal:owner/repo` (the duplicate #60076), and any
non-standard host. Before #53913, the flow used the link git prints in
the push output, which works regardless of host, so this is a regression
for anyone not pushing to a plainly-recognized GitHub URL.

## Solution

`git push` prints a `remote:` line with the hosting provider's
create-PR/MR URL (GitHub: "Create a pull request for '\<branch>' on
GitHub by visiting:", GitLab: "To create a merge request for \<branch>,
visit:", Bitbucket: "Create pull request for \<branch>:"), and we
already hold the push `RemoteCommandOutput`.

- `remote_output.rs`: add `extract_pull_request_url`, which scans the
push stderr for the first `http(s)` URL on a `remote:` line tied to a
create-PR/MR prompt. It ignores unrelated URLs (for example the OpenSSH
post-quantum warning line).
- `git_panel.rs`: capture that URL in `show_remote_output` into
`pending_pull_request_url`, and prefer it in `create_pull_request`,
falling back to the existing provider construction only when the push
output had no link. The cached URL is consumed once (`take()`) and
cleared when the active repo, the active branch/head, or the pending
remote operation changes, so a later `git: Create Pull Request` action
never opens a stale URL from an earlier push.

Recognized GitHub remotes are unaffected: they still get a link from the
push output, with the provider path as the fallback.

## Testing

Unit tests in `remote_output.rs` cover `extract_pull_request_url` for
the GitHub, GitLab, and Bitbucket prompt formats, the no-link case
(returns `None`, so the provider fallback runs), and an output
containing an unrelated URL that must not be mistaken for the PR link.
The existing remote-operation test also asserts the cached URL is
cleared when a new operation starts.

`cargo test -p git_ui remote_output` passes (5 tests). Tested on macOS.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed "Create Pull Request" button in the toast shown after `git:
push` failing for repositories on unrecognized Git hosts by using the
link printed in the push output.
- Fixed the button shown on the toast after `git: push` for GitLab
branches with an existing merge request. It now shows "View Merge
Request" and links to the existing merge request.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: dino <dinojoaocosta@gmail.com>
2026-07-07 17:14:37 +00:00
Philipp Schaffrath
56009f39bb
gpui_wgpu: Fix wgpu renderer teardown panic (#60160)
# Objective

Opening and closing windows quickly on Linux could crash the renderer
with a `GPU resources not available` panic.

The `gpui_wgpu` renderer keeps its GPU resources in an `Option` that is
cleared when a window is torn down, and also while device-loss recovery
is pending. On Wayland the window's `Drop` releases those resources
synchronously but defers unregistering the surface to a later task, so a
compositor resize or transparency event can still reach the renderer in
that short gap. `update_drawable_size` and `update_transparency` assumed
the resources were always present and called an accessor that panics
when they are not.

I am not certain if there is a good case to reproduce this in zed, but I
had encountered it in my own GPUI app.

## Solution

Make `update_drawable_size` and `update_transparency` tolerate missing
GPU resources by skipping the surface reconfiguration instead of
panicking, matching how the rest of the renderer already guards this
state. The requested size and alpha mode are still recorded before the
guard, so they take effect if the renderer's resources are recreated,
for example after device-loss recovery.

## Testing

Tested on Linux with Wayland.

- Reproduced by opening and closing windows quickly, which previously
panicked with `GPU resources not available`. With this change the panic
no longer occurs. This was reproducible on a debug build or a weak/slow
device.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes (not sure if worth mentioning):

- Fixed a crash that could occur when opening and closing windows
quickly on Linux

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2026-07-07 16:52:35 +00:00
Adrian Wowk
5a7d414a23
fs: Skip parent watch for poll watcher symlink targets (#57049)
# Problem

Since the release of the new git UI, when `~/.gitconfig` on a remote
server is a symlink pointing to a file on a virtual filesystem (a common
setup when using [OrbStack](https://orbstack.dev/) on macOS), Zed fails
to connect with "Timed out pinging remote client".

# Cause

When setting up a file watcher for gitconfig, `fs::watch()` reads the
symlink target and adds its parent directory to the poll watcher.
`notify::PollWatcher::watch()` does a full synchronous recursive
directory scan at registration time to build an initial snapshot. If the
parent is something like a Mac home directory mounted via virtiofs, that
scan blocks the server's main thread long enough that it can't respond
to the initial ping within the 5 second timeout.

# Solution

The fix I implemented for this was to skip the parent directory watch
when using a poll watcher. As far as I can tell, it's redundant in the
poll case since the poll watcher detects changes by periodically reading
metadata at the registered path, so watching the parent doesn't add
anything for change detection. From my limited testing this seems to
work fine but if someone with more experience in this part of the
codebase would like to weigh in, that would be very much appreciated.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable

Release Notes:

- Fixed remote SSH connections timing out when `~/.gitconfig` is a
symlink to a file on a virtual filesystem
2026-07-07 16:26:30 +00:00
Dino
3cbe4c298e
Add missing panels to View menu (#60356)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
# Objective

Ensure that all existing panels have corresponding menu items in the
"View" menu. I was onboarding a friend to Zed yesterday that was having
a hard time figuring out how to interact with the Agent. Although he did
open the "View" menu, I noticed that the Agent panel item was missing
from there, making it hard for new users to discover it exists.

## Solution

* Add both "Agent Panel" and "Git Panel" entries to the menu items for
the "View" app menu.
* Update the action used for the "Terminal Panel" menu item from
`terminal_panel::ToggleFocus` to `terminal_panel::Toggle` to ensure we
display a shortcut for this menu item.
* Another valid option would be to update the default keymap to use
`terminal_panel::ToggleFocus` instead but that would probably break
existing user's expectations that the default shortcut toggles the
terminal panel, instead of toggling its focus.
* Introduce `zed_actions::git_panel` to be able to extract its
`ToggleFocus` action, following the existing pattern.

### Next Steps

It's worth noting that, even though there's now an "Agent Panel" item
mapped to the `assistant::ToggleFocus` action, its default keybinding is
not displayed (at least on macOS), because of the way it's defined as
`cmd-?` . Using `cmd-shift-/` instead doesn't work, so we'll likely have
to update `MacKeyboardMapper` to allow mapping between shifted and
unshifted keys equivalent, that is, when `?` is detected, it is able to
determine that, in the user's layout that is the result of `shift-/`.

## Testing

Manually tested, screenshots can be seen in the "Showcase" section.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable

## Showcase

<details>
  <summary>Before</summary>

<img width="488" height="946" alt="CleanShot 2026-07-03 at 14 24 35@2x"
src="https://github.com/user-attachments/assets/58b0497c-2929-4da3-86b6-a2e0ce0c5ca4"
/>

</details>

<details>
  <summary>After</summary>

<img width="524" height="1116" alt="CleanShot 2026-07-03 at 14 24 59@2x"
src="https://github.com/user-attachments/assets/8e2c6320-97ad-4620-b595-182f6d0ded81"
/>

</details>

---

Release Notes:

- Added "Agent Panel" and "Git Panel" items to the "View" menu
2026-07-07 14:38:47 +00:00
Lukas Geiger
bc29bcfe72
git: Load buffer git diff bases with a single batched git process (#59357)
# Objective

Whenever the git repository state is updated on disk (e.g., via staging,
committing, branch switching, or stashing), `reload_buffer_diff_bases`
is scheduled to reload the diff for all active buffers. This causes 2
git processes to be spawned for each open file which can become
noticeable when many files are open

5e32405669/crates/project/src/git_store.rs (L5179)

## Solution

This PR introduces `load_revisions` which uses a single `git cat-file
--batch` command to compute the diff for all files in the same git
process. This prevents the need to sequentially schedule 2 git
subprocesses per open buffer.

I also changed `load_index_text` and `load_commited_text` to rely on
`load_revisions` which simplifies the code.

## Testing

I added a unittest and manually verified that Zed now only runs a single
`git cat-file --batch` command instead of 2 `git show` processes per
open buffer.
On macOS I viewed the currently running git processes using:
```shell
sudo eslogger exec | jq --unbuffered -r '
    select(.event.exec?.target?.executable?.path? | strings | contains("git")) |
    (.event.exec?.args? // []) | join(" ")
  '
```

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

/cc @Veykril

Release Notes:

- Reduced number of git processes for calculating diff of open buffers
when the repo state changes on disk
2026-07-07 14:02:31 +00:00
Ruslan Semagin
d564495dde
git_ui: Show tags in Git panel history (#60534)
# Objective

- Make Git Panel History show commit tags so release/version markers are
visible without opening the full Git graph or commit details.

  ## Solution

- Store commit history entries with both the commit SHA and tag names
from existing git graph data.
  - Render tag names as muted chips next to the commit subject.
- Limit visible tags to 3 per commit and show `+N` for additional tags.

  ## Testing

  - Ran `cargo check -p git_ui`.
  - Manually verified on Linux with `script/zed-local --stateful .`.
  - Confirmed tags appear in Git Panel History.

  ## Self-Review Checklist:

  - [x] I've reviewed my own diff for quality, security, and reliability
  - [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and [icon]

(https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
  - [ ] Tests cover the new/changed behavior
  - [x] Performance impact has been considered and is acceptable

  ## Showcase

Git Panel History now shows commit tags inline as muted chips next to
the commit subject.

  ---

  Release Notes:

  - Added tag labels to the Git Panel commit history.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2026-07-07 13:50:03 +00:00
Ben Brandt
fc827a218e
Update issue ranking script dependencies (#60345)
Release Notes:

- N/A
2026-07-07 12:49:19 +00:00
Danilo Leal
a94fa5cf19
git_graph: Add design adjustments (#60469)
This PR adds adjustments to the tree view, making it more consistent
with all other tree view displays in the app (e.g., displaying indent
guides, removing chevron toggle, etc.), and also fixes an issue where
the commit message scrollbar was scrolling up with the message.

Release Notes:

- N/A
2026-07-07 12:15:07 +00:00
Lukas Wirth
f360136f19
project: Create LSP file watcher on the background (#60530)
Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-07 11:48:14 +00:00
Ben Brandt
c05b439174
acp: Show descriptions for elicitation options (#60527)
<img width="683" height="781" alt="image"
src="https://github.com/user-attachments/assets/bcf1bc75-9bb4-4ddf-88d8-c09a4aa3f34b"
/>


Release Notes:

- N/A
2026-07-07 11:45:59 +00:00
Lillian Rose
fbd911ed3e
Limit SVG Pixmap size to avoid GPUI texture allocation errors (#56468)
Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
(Arguably no, but it's an okay compromise methinks)
- [ ] Tests cover the new/changed behavior (I'm unsure how one would
properly test this, sorry!)
- [x] Performance impact has been considered and is acceptable

Closes #56466

To be completely honest I don't know if this is a good fix or not, it
does fix the problem I was running into where opening the large mermaid
diagram would blow up VRAM. It doesn't look amazing visually but I would
consider this behavior better, if it's not a good fix then that's okay:)

I chose 8192 because 8192 only brings VRAM usage up ~100MB in my testing
while 16384 brought my VRAM usage up to about 1GB from 150-200MB, for
lower end systems this seems unacceptable.

Before:
I can't take a screenshot of the before at this point because it eats my
system VRAM & Memory too fast. As a text description; It would show a
large empty rectangle where the mermaid diagram should be and blow up
Zed's VRAM usage from ~300MB to ~22GB (all of the available VRAM in my
system)

After:
<img width="1698" height="763" alt="image"
src="https://github.com/user-attachments/assets/62eb7c95-cca8-43f9-8257-c7e529f26e8d"
/>
<img width="1000" height="31" alt="image"
src="https://github.com/user-attachments/assets/4315c029-3cdd-44f6-ac78-971d125ab700"
/> (Up from ~150MB), the 257MiB figure is the GPU Memory.

Release Notes:

- N/A?

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2026-07-07 11:04:15 +00:00
迷渡
961f4f2024
gpui: Refresh mouse position after bounds changes (#60421)
## Summary

- Refresh GPUI's cached mouse position when window bounds change so
hover hit-testing uses the current cursor position after live resize.
- Return X11 mouse positions in window-relative logical pixels to keep
`PlatformWindow::mouse_position()` consistent with other backends.

Fixes #57354

## Testing

- `cargo fmt -p gpui -p gpui_linux`
- `cargo check -p gpui_linux`
- `cargo check -p gpui`

## Suggested .rules additions

- In GPUI platform backends, `PlatformWindow::mouse_position()` should
return window-relative logical pixels; use separate APIs or fields for
global/device-pixel coordinates.

Release Notes:

- Fixed incorrect hover state while resizing GPUI windows.
2026-07-07 10:59:45 +00:00
Xiaobo Liu
693962917b
gpui: Fix clear drag overlay when external drag ends outside window (#45759)
Release Notes:

- Fixed clear drag overlay when external drag ends outside window



When dragging files from macOS Finder over the project panel and then
dragging back to Finder, the drag overlay remained visible because the
drag state was not properly cleaned up.

The root cause was that only `draggingExited:` was handled, but not
`draggingEnded:`. On macOS:
- `draggingExited:` is called when the drag leaves the window area
- `draggingEnded:` is called when the drag operation ends entirely

When a user drags a file back to Finder and drops it there,
`draggingEnded:` is called but was not being handled.

---------

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2026-07-07 10:39:29 +00:00
hiro / THETIME
59c021d8dc
copilot_ui: Fix Copilot sign-in window focus on Hyprland (#59933)
# Objective

The Copilot sign-in dialog was created without an `app_id` or window
title, resulting in an empty WM class/title on Linux. Tiling window
managers with class-based no-focus rules (like Hyprland's default
configuration in Omarchy) treat such windows as anonymous popups and
refuse to focus them, making the dialog impossible to interact with.

## Solution

Set both `app_id` and window title on the Copilot code verification
window, following the established pattern used in other UI components
like `agent_ui` and `settings_ui`.

Added `release_channel` as a dependency to
`crates/copilot_ui/Cargo.toml` and called
`app_id(ReleaseChannel::app_id(cx))` and `window_title("Use GitHub
Copilot in Zed")` in `open_copilot_code_verification_window`.

## Testing

Verified on Hyprland (Omarchy) by inspecting window properties with
`hyprctl clients`:

**Before (empty class/title):**

```
Window 5606ee1dd280 -> :
    class: 
    title: 
    acceptsInput: 0
```

**After (with proper class/title):**

```
Window 5606ee22b660 -> Use GitHub Copilot in Zed:
    class: dev.zed.Zed-Dev
    title: Use GitHub Copilot in Zed
    acceptsInput: 1
```

The dialog now receives keyboard focus and mouse input correctly on
Hyprland. I tested on Linux only; this fix lives in the window creation
call so it is a no-op on macOS and Windows where `app_id` is ignored.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed Copilot sign-in window not being focusable on Hyprland and
similar tiling window managers

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-07 10:27:57 +00:00
Lukas Wirth
2eeca73e66
auto_update: Fix installer temp dirs leaking on macOS (#60528)
The unmount of the update disk image was made async-and-detached in
#38867, which introduced a race: the installer TempDir was dropped
(running remove_dir_all) while the DMG was still mounted inside it. The
removal failed silently, leaking a zed-auto-update* dir containing the
~140 MB DMG in /private/var/folders on every update.

Now the unmount is awaited before the temp dir is dropped (installation
already runs on the background executor since #58767, so this no longer
blocks the UI), with the Drop impl kept as a safety net for early exits
and cancellation. Additionally, stale installer dirs older than 24 hours
are swept from the temp dir when update polling starts, so existing
accumulated leaks get reclaimed.

Closes FR-104
Closes #58835

Release Notes:

- Fixed the macOS auto-updater leaking a copy of the downloaded update
in the system temp directory on every update, and added cleanup of
previously leaked files.
2026-07-07 10:23:28 +00:00
Vlad Ionescu
452e1cb273
opencode: Model updates + fixes (#60526)
**TL;DR**: model updates + reasoning levels + fixes discovered when
working on https://github.com/zed-industries/zed/pull/60373

# Objective

Since the model auto-discovery PR was
[cancelled](https://github.com/zed-industries/zed/pull/60373#issuecomment-4886521448),
here is a manual model list update! I also copied the stand-alone
bugfixes/enhancements from that PR.

## Solution

A lot of manual work 😅 

**OpenCode Zen**: 
- added Fable 5 and Sonnet 5
- added models that were previously only available on OpenCode Go: GLM
5.2, Kimi K2.7 Code, and Minimax M3
- added reasoning levels for all models. I started from the data on
[`Models.dev`](https://models.dev) (the `/api.json` raw data), and then
I matched that with what is shown in the OpenCode CLI and what I know to
be true

**OpenCode Go**:
- added reasoning levels for GLM 5.2

**OpenCode in general**:
- added `protocol` validation for the settings, by moving from a random
`String` to an `enum`, for both nicer error messages (random strings or
typos will get an error instead of using `openai_chat` by default) and
to avoid issues like [folks saying non-existent protocols are a
thing](https://github.com/zed-industries/zed/issues/56869#issuecomment-4550154554)
- enabled parallel tool calls by default. As per [OpenCode developer on
Discord](https://discord.com/channels/1391832426048651334/1471233160993050918/1472020924881702912),
_"almost all models worth using support parallel tool calling
natively"_. Manual tests confirmed all OpenCode Go models support this
correctly (and was enabled by default on the OpenCode side for all but 1
model). I initially wanted to skip this from the release notes, but I
added it so folks are aware of it in case any issues are caused by this
being enabled for all models
- allegedly fixed Google thinking since reasoning levels / thinking
effort levels were added for Google models and an auto-checker LLM
highlighted that was not properly configured
- added support for the new-ish `supports_disabling_thinking` so
thinking-only models don't get a no-impact toggle to disable thinking

I have no idea if any of the Free models will disappear in 2 days or
not, so I did not update those 🤷 (as per decision in
https://github.com/zed-industries/zed/issues/56869#issuecomment-4466637648)

## Testing

The Zen and Google changes were not tested as I don't have a Zen
subscription and I stubbornly refuse to get one.

The Free&Go changes were tested by running a "_rename this variable for
me. add a function. delete the function_" test with a few different
models.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:
- Agent: OpenCode settings now validate `protocol` values
- Agent: OpenCode only shows the "Disable thinking" toggle if thinking
can indeed be disabled/enabled
- Agent: OpenCode models now enable parallel tool calls by default
- Agent: Updated OpenCode Zen models (added Fable 5, Sonnet 5, GLM 5.2,
Kimi K2.7 Code, and Minimax M3)
- Agent: Added OpenCode Go GLM 5.2 reasoning effort levels
- Agent: Added reasoning effort levels for all OpenCode Zen models
- Agent: Fixed thinking for OpenCode Zen Google models
2026-07-07 10:16:42 +00:00
excited gui
64c55b038f
Change OpenAI compatible URL placeholder in edit prediction settings (#58771)
Fixed my PR #58753. 

Change the url placeholder from http://localhost:11434 to
http://localhost:8080/v1/completions to match the URL endpoint in the
[docs](https://zed.dev/docs/ai/edit-prediction)
```json
{
  "edit_predictions": {
    "provider": "open_ai_compatible_api",
    "open_ai_compatible_api": {
      "api_url": "http://localhost:8080/v1/completions",
      "model": "deepseek-coder-6.7b-base",
      "prompt_format": "deepseek_coder",
      "max_output_tokens": 512
    }
  }
}
```
Note: http://localhost:8080/v1/completions/ with an extra / does not
work.


Added the constants OPEN_AI_COMPATIBLE_API_URL_PLACEHOLDER and
OPEN_AI_COMPATIBLE_MODEL_PLACEHOLDER.

### Initial Issue
I noticed that using http://localhost:8080 doesn't work with llama.cpp.
Giving errors like this from (zed: open log):
```
2026-06-06T17:55:39+01:00 ERROR [crates/edit_prediction/src/edit_prediction.rs:2464] custom server error: 404 Not Found - {"error":{"message":"File Not Found","type":"not_found_error","code":404}}
```
After reading the docs above, I found out that I had to add
/v1/completions to the end.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:
- N/A
2026-07-07 09:53:16 +00:00
Shreyash Saitwal
811fe501a7
workspace: Fix missing icons on non-terminal tabs during drag (#53637)
Previously, only terminal tabs displayed their icon while being dragged,
all other tab types showed just the label, making the drag preview
inconsistent with the actual tab appearance.


https://github.com/user-attachments/assets/7c055945-79d6-4f6e-8969-279a4fb1ff58

This PR fixes that by rendering each tab's icon alongside its label, so
the dragged preview now accurately reflects the tab regardless of type.


https://github.com/user-attachments/assets/3ad5a99b-5ba7-41d2-a6b3-51101b1f9650

Release Notes:

- Fixed missing icons on non-terminal tabs when dragging

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-07 09:29:38 +00:00
Lysastriel
71b3b9b887
gpui_macos: Guard start_display_link against nil screens (#60419)
# Objective

Prevent a macOS crash in GPUI when AppKit temporarily reports that a
visible window has no associated `NSScreen` during display
reconfiguration.

The crash can happen on this path:

```text
NSScreen::deviceDescription
gpui_macos:🪟:display_id_for_screen
gpui_macos:🪟:MacWindowState::start_display_link
gpui_macos:🪟:window_did_change_screen
```

`start_display_link` checked the window occlusion state before creating
a display link, but it still assumed `NSWindow.screen()` was non-null.
During display changes, sleep/wake, lid close/open, or monitor
reconfiguration, AppKit can transiently return `nil` for `screen`, and
passing that into `NSScreen::deviceDescription` aborts with a null
pointer dereference.

I observed this on macOS 26.5 after the system entered a rare display
state: once in that state, running GPUI applications would crash
immediately after wake. I have not identified the exact OS/display
condition that triggers it, so the full wake/reconfiguration scenario is
not deterministic, but the crash report consistently points to
`NSWindow.screen()` being `nil` when `start_display_link` tries to
create a display link.

## Solution

Make `display_id_for_screen` explicitly handle a null `NSScreen` by
returning `None`.

Callers now handle that case by either:

- returning early from `start_display_link`, because there is no valid
display id to create a display link for yet
- skipping a null screen while iterating `NSScreen::screens`

The normal non-null screen path is unchanged.

This keeps the nil-screen guard at the FFI boundary where
`NSScreen::deviceDescription` is called.

## Testing

Tested on macOS 26.5.

Commands run:

```bash
cargo fmt --check -p gpui_macos
cargo test -p gpui_macos display_id_for_screen_returns_none_for_null_screen
cargo check -p gpui_macos
```

Added a unit test covering the new boundary behavior:
`window::tests::display_id_for_screen_returns_none_for_null_screen`

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed a macOS crash that could occur when display configuration
changes while a GPUI window is temporarily not associated with a screen.
2026-07-07 09:02:29 +00:00
迷渡
7194f987a7
Fix disabled Windows window controls (#60440)
## Summary

- Store Windows window minimizable and resizable capabilities on
`WindowsWindowInner`.
- Prevent custom titlebar hit testing from returning minimize/maximize
button hit targets when the corresponding capability is disabled.
- Swallow disabled native non-client minimize/maximize button mouse
events so they cannot fall through to the default window procedure or
GPUI's manual `ShowWindowAsync` handling.

Fixes #52067.

## Validation

- `cargo fmt --package gpui_windows`
- `cargo check -p gpui_windows`
- `git diff --check`

Release Notes:

- Fixed disabled minimize and maximize window controls still activating
on Windows.
2026-07-07 09:01:58 +00:00
Ankan Misra
20a93f6195
gpui_macos: Fix glyph rendering when fonts share a PostScript name (#57250)
On macOS, `load_family` was inserting every font into
`font_ids_by_postscript_name` without checking for duplicates. When two
installed font files claim the same PostScript name — typically an older
Geist Mono left behind alongside the current brew cask — the second
insert overwrote the first. After shaping, `id_for_native_font` looked
up that PostScript name and got back the *other* font's `FontId`, so the
rasterizer drew glyphs from the wrong table
See #55472 for the screenshot

This adds a local `HashSet` to dedup within a single `load_family` call.
Scoping it to one call (rather than the global map) matters: the same
family gets reloaded under different `FontKey`s when `FontFeatures` or
`FontFallbacks` change, and a global check would skip every font on the
reload and break weight selection.

Cross-family PostScript name collisions and CoreText fallback
substitutions that re-introduce a conflict are out of scope here

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed glyph rendering when fonts share a PostScript name on macOS

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2026-07-07 08:59:55 +00:00
Nick Mosher
e7803a88f5
gpui: Add ParentElement impl for AnimationElement (#54145)
Hello, I've been loving using GPUI! Recently I noticed that calling
`.with_animiation()` would give an `AnimationElement<E>` which did not
allow `.child()` to be called on it. It was a quick fix, I hope it's a
quick easy merge but let me know if you'd like anything changed :)

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2026-07-07 08:59:43 +00:00
Philipp Schaffrath
a29c0d41f4
gpui: Add input region support for Wayland windows (#60161)
# Objective

Wayland windows have no way to restrict which parts of the surface
accept pointer and touch input. This adds support for setting an input
region, so events outside it pass through to whatever is below the
window. This is useful for shaped or partially click-through windows.


Clicks in green area can pass through the window, clicks in red area do
not:
<img width="1057" height="395" alt="image"
src="https://github.com/user-attachments/assets/2039af62-e43b-4834-b877-edad2a8f5ccf"
/>

## Solution

Add `Window::set_input_region`, which takes `Option<&[Bounds<Pixels>]>`:

- `Some(rects)` restricts pointer and touch input to the union of the
rectangles, in window coordinates.
- `Some(&[])` is an empty region, so the window receives no input at all
and is fully click-through.
- `None` resets the region to the default, so the whole window receives
input again.

On Wayland this maps to `wl_surface.set_input_region`, building a
`wl_region` from the rectangles or clearing it for `None`, and commits
so the change applies immediately rather than waiting for the next
frame. The method is a no-op on other platforms.


## Testing

Tested on Linux with Wayland.

- Tested in my own GPUI application, which uses a fullscreen layer for
overlays while allowing clicks outside of the rendered elements to be
passed through to the underlying windows.
- No automated test was added, since this calls through to the
compositor and is checked by observing input routing.


## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- N/A
2026-07-07 08:54:07 +00:00
Remco Smits
fcd0f76952
git_ui: make git graph columns toggleable (#59850)
# Objective

TODO

## Solution

TODO

## Self-Review Checklist:

- [ ] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable

## Showcase


https://github.com/user-attachments/assets/ae1e2759-2ae9-4c6f-aba3-b6b557c673bc

---

cc @Anthony-Eid 

Release Notes:

- Git graph: Make columns toggleable

---------

Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2026-07-07 08:01:05 +00:00
counterfactual5
a956add0b6
Fix MCP tools with $ref/$defs being silently rejected (#60165)
Closes #60162.

MCP servers whose tool `inputSchema` uses `$ref`/`$defs` (e.g., Notion
MCP v2.x, and any server using Zod/Pydantic-generated schemas) are
silently rejected with:

```
ERROR Schema cannot be made compatible because it contains "$ref"
```

The affected tools are dropped from the agent panel — the user never
sees them and there is no user-visible error.

## Root cause

`adapt_to_json_schema_subset` rejects any schema containing `$ref` via
`UNSUPPORTED_KEYS`:

```rust
const UNSUPPORTED_KEYS: [&str; 4] = ["if", "then", "else", "$ref"];
```

This is hit by every provider that uses `JsonSchemaSubset` format
(Google Gemini, xAI Grok, OpenAI-compatible proxies, Vercel AI Gateway,
Copilot Chat for Google/xAI vendors, OpenRouter for gemini/grok models).
Providers using `JsonSchema` (Anthropic direct, OpenAI direct) don't hit
this check — `$ref` is passed through to the API, which may or may not
handle it correctly.

This is not an edge case. Every modern MCP server using Zod
(TypeScript), Pydantic (Python), or JSON Schema with shared definitions
generates `$ref`/`$defs` in tool schemas.

## Fix

Add a `resolve_refs` step in `adapt_schema_to_format` that dereferences
all `$ref` pointers using the document's own `$defs` (or legacy
`definitions`) map, making the schema self-contained before
format-specific processing. Applied at the entry point so both
`JsonSchema` and `JsonSchemaSubset` formats benefit.

**Scope note:** previously `JsonSchema` providers (Anthropic direct,
OpenAI direct) received the raw `$ref`/`$defs` and were expected to
handle it themselves — which most do not. After this change, both paths
receive a self-contained schema with refs inlined. This is intentional:
it fixes the same root cause for both paths and avoids provider-specific
behavior divergence.

Supported `$ref` forms:
- `#/$defs/<name>` (JSON Schema draft 2019-09+)
- `#/definitions/<name>` (draft 4-7 legacy)

Edge cases:
- **Nested `$ref`** (definition references another definition): resolved
recursively.
- **Sibling properties alongside `$ref`** (e.g. `{ "$ref": "...",
"description": "..." }`, legal under draft 2019-09+): merged onto the
resolved definition, with siblings overriding the definition's keys.
- **Cyclic references** (A → B → A, or self-referential schemas like a
Tree node): replaced with an empty schema `{}` ("any JSON value"). The
tool still works, just without type info for that recursive field.
- **Unsupported `$ref` forms** (e.g., external URLs): returns an error
with a clear message.
- **Missing definition target**: returns an error naming the missing
ref.

## Testing

Added 10 unit tests in `crates/language_model_core/src/tool_schema.rs`
that cover the patterns produced by Zod/Pydantic-generated MCP schemas:

- `test_refs_are_resolved_via_adapt_schema_to_format` — basic `$ref` →
`$defs` resolution
- `test_refs_in_defs_are_resolved` — nested `$ref` (definition
references another definition)
- `test_refs_in_array_items_are_resolved` — `$ref` inside `array.items`
- `test_legacy_definitions_prefix_is_supported` — old
`#/definitions/<name>` prefix
- `test_schema_without_defs_is_unchanged` — schemas with no `$defs` are
unaffected
- `test_refs_fail_for_unsupported_prefix` — external URL refs error
clearly
- `test_refs_fail_for_missing_definition` — missing target errors
clearly
- `test_cyclic_refs_are_replaced_with_empty_schema` — A → B → A cycle
replaced with `{}`
- `test_self_referential_ref_is_replaced_with_empty_schema` — Tree-like
self-ref replaced with `{}`
- `test_ref_sibling_properties_are_preserved` — sibling properties
alongside `$ref` are merged onto the resolved definition

Existing tests (which call `adapt_to_json_schema_subset` and
`preprocess_json_schema` directly) are unaffected — the fix is additive
at the `adapt_schema_to_format` level.

## Disclosure

I used an LLM to help draft the implementation and tests. I reviewed and
understand the change — it adds one new function (`resolve_refs`) with
two helpers (`parse_ref`, `resolve_refs_recursive`), plus unit tests.

Release Notes:

- Fixed MCP tools with `$ref`/`$defs` in their `inputSchema` being
silently rejected by providers using the JSON Schema Subset format
(Google Gemini, xAI Grok, OpenAI-compatible proxies, etc.). Tools from
servers like Notion MCP v2.x, and any server using Zod or
Pydantic-generated schemas, now work correctly.

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2026-07-07 07:56:02 +00:00
Smit Barmase
001bda1a46
project: Fix ref-match rust completions dropping the leading & (#60521)
Closes #56973, Follow-up for
https://github.com/zed-industries/zed/pull/56976

When a completion's `additionalTextEdits` contained a zero-width
insertion touching the edge of the primary edit, the overlap check
treated it as overlapping and silently skipped it. This broke
rust-analyzer's ref-match completions (`&foo`), which deliver the `&` as
a zero-width additional edit at the primary edit's start (#56973). The
same root cause previously broke auto-imports at the very start of a
file (#26136), which was worked around in #37746 with a special case for
edits at position (0, 0).

This PR replaces that special case with a general rule: a zero-width
additional edit only overlaps the primary edit when it falls strictly
inside it. i.e. touching the boundary is fine.

This one check handles both the file-start auto-import case and the `&`
ref-match case, so the (0, 0) workaround from #37746 is removed.
Non-insertion edits still go through the original overlap check from
#1871.

Added three regression tests:
- the file-start auto-import (#26136)
-  the `&` insertion at the primary edit's start (#56973)
- overlapping edits being skipped while non-overlapping ones apply
(general skip case)

Release Notes:

- Fixed rust-analyzer completions like `&some_var` inserting only the
variable name and dropping the leading `&` when accepted.
2026-07-07 07:49:53 +00:00
Apoorva Verma
d9ada8487b
Fix hard-tab block autoindent skipping unindented lines (#60406)
# Objective

Fixes #59979

With hard tabs on, expanding a multi-line snippet only re-indents the
lines that already start with a tab. Anything with no leading whitespace
(closing brackets, mostly) stays at column 0.

## Solution

Block autoindent shifts each line by the first line's delta, but only
when the line's indent kind matches the target kind. A line with no
indentation defaults to `IndentKind::Space`, so under hard tabs it never
matches and gets skipped. An empty indent doesn't really have a kind, so
I let it adopt the target kind before the check. Lines that actually
have space indentation in a tab buffer are still left alone. Normalizing
those felt like a bigger question than this bug, happy to look at it
separately if wanted.

## Testing

New `test_autoindent_block_mode_with_hard_tabs`, same shape as the
snippet in the issue. Fails on main (closing brace stays at column 0),
passes with the fix. `cargo test -p language` and `-p editor` are green,
clippy and fmt too.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed multi-line snippets leaving unindented lines at column 0 when
`hard_tabs` is enabled.
2026-07-07 07:45:54 +00:00
Richard Feldman
28c2e7d1e4
Fix tree-sitter-markdown scanner serialize buffer overflow (#60312)
Zed bundles the markdown grammar's block scanner natively, and its
`serialize()` `memcpy`s the open-block stack into tree-sitter's fixed
1024-byte serialization buffer with no bounds check. Markdown with
roughly 255+ nested blocks overflows that buffer, and because it sits at
the front of `struct TSParser`, the overflow clobbers the adjacent
parse-stack pointer and heap. Debug builds of the tree-sitter runtime
catch this with an assertion, but release builds like Zed's have no
check and silently corrupt parser memory — which is why this surfaced as
wild crashes deep in tree-sitter's parse stack rather than clean
failures. Still open upstream as
tree-sitter-grammars/tree-sitter-markdown#243.

This PR points `tree-sitter-md` at a `zed-industries` fork whose
`serialize()` refuses to write state that doesn't fit (bounded by the
same running counter the header writes advance, so the check can't
drift). The scanner then deserializes to a fresh state, and the
pathologically nested region surfaces as ordinary tree-sitter `ERROR`
nodes — visible, safe degradation for an adversarial input class,
deliberately chosen over the two alternatives: truncating the block
stack would deserialize into a plausible-but-wrong state and produce
silently incorrect trees, and the scanner ABI offers no error channel at
all (`serialize()` returns a length into a fixed buffer; there is no way
to fail a parse). A nesting-depth cap at block-open time would give
fully deterministic semantics, but that's a behavior change across 13
scanner call sites that belongs upstream, not in a hotfix fork.

The pinned branch is upstream's `9a23c1a9` (the revision Zed already
pinned) plus exactly two commits, for easy review: the guard
(zed-industries/tree-sitter-markdown@179422edf8)
and regression tests
(zed-industries/tree-sitter-markdown@b596e73728).
The deep-nesting test aborts on the unguarded scanner and passes with
the guard; a moderate-nesting test pins that inputs fitting the buffer
still parse cleanly. The same change is also up as
zed-industries/tree-sitter-markdown#1 into the fork's default branch
(`split_parser`), so future pin bumps don't lose it; if upstream fixes
#243, we can drop the fork entirely on the next bump.

Closes FR-115

Release Notes:

- Fixed a potential crash when editing Markdown with deeply nested
blocks
2026-07-07 07:28:33 +00:00
drbh
c31b2b0dc7
Git partially staged changes (#46541)
This PR explores the addition of a new feature and UI to improve
visibility into partially staged commits.

Currently, the Git panel shows tracked and untracked changes, but it
does not clearly distinguish between staged and unstaged changes.

As a result, it’s difficult to quickly see which changes are not staged
in the current UI. Both staged and unstaged changes are combined into
the `Uncommitted Changes` multibuffer. This developer experience differs
from other editors, most notably VS Code; which presents separate Staged
Changes and Changes lists.

### Staged and unstaged diffs in multibuffers

This PR introduces an alternative UI for unstaged changes that aligns
with the overall Zed experience. Instead of showing changes on a
per-file basis, staged and unstaged diffs are each displayed in their
own multibuffers, similar to how `Uncommitted Changes` currently works.

For example the following screenshot shows the current `Uncommitted
Changes` on the left, the `Staged Changes` in the middle and the
`Unstaged Changes` buffer on the right for comparison

<img width="1408" height="859"
src="https://github.com/user-attachments/assets/aa709f7a-041d-4cb1-95d6-84c0f5fff688"
/>

### Indicators/interactions

The new multibuffers can be opened in two ways:

1. Via a new `U` chip, which appears when a file has unstaged changes
2. Via new menu options

(See screenshots below for both interaction paths.)


<table>
  <tr>
    <td style="text-align: center; vertical-align: top;">
      <p>via the chip</p>
      <img
        height="400"

src="https://github.com/user-attachments/assets/3ef69f02-b787-499c-959a-25f50b3728e8"
        alt="Via the chip"
      />
    </td>
    <td style="text-align: center; vertical-align: top;">
      <p>via the menu</p>
      <img
        height="400"

src="https://github.com/user-attachments/assets/f5be8b6d-ccdc-4420-bd29-75570b558016"
        alt="Via the menu"
      />
    </td>
  </tr>
</table>

### Design goals
- minimally intrusive UI changes (small new badge and menu items)
- adhere by Zed'ism (use multibuffer where possible)
- avoid disabling any current interactions (Uncommitted Changes ui is
unchanged)
- avoid introducing an app level view mode (no new settings needed)

### Experience goals
- make it easy to see what changes are not staged
- make it easy to see that a file has unstaged changes (avoid developers
accidently leaving out changes in a commit; a personal issue that I have
when using Zed)
- elegantly handle large file's unstaged changes (follows the same
collapse and expanding seen in `Uncommitted Changes`)

### How to try

- Clone the repo and run `cargo run`
- Make a change to a file and stage it
- Make another change to the file (the `U` indicator will appear)
- Click the `U` to see the unstaged view

### Open questions/rough edges
- [ ] determine if this user experience is useful for others
- [ ] ensure all interactions work as expected (response to all update
cases)

In general I'm really interested in hearing the community's feedback
about this interface, more than happy to make any changes or explore a
different solution!

### Related issue:
- https://github.com/zed-industries/zed/pull/36646
- https://github.com/zed-industries/zed/issues/26560

Release Notes:

- Support partially staged commit multibuffers via a staged and unstaged
changes view.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cole Miller <cole@zed.dev>
2026-07-07 06:55:29 +00:00
Richard Feldman
872ca8fef5
Add license symlinks to lint test fixture crates (#60505)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Stacked on #60502 and targets it — #60502 must land first (its
orchestrator fix is what lets this `tooling/lints/`-touching PR go green
rather than tripping the `rdeps(lints)` nextest filter). Re-creates the
symlinks from #60504, which was accidentally merged into #60502's branch
and reverted.

Follow-up to #60468, which added a `LICENSE-APACHE` symlink to the
`tooling/lints` crate but not to the `test_fixture` sub-crates. Those
five sub-crates each carry a `Cargo.toml`, so `script/check-licenses`
requires a `LICENSE-GPL`/`LICENSE-APACHE` symlink in each, and
`check_licenses` fails on any PR that changes `Cargo.lock`. This adds
`LICENSE-APACHE` symlinks to the five fixture crates, matching #60468;
`script/check-licenses` passes locally afterward.

Release Notes:

- N/A
2026-07-07 01:35:06 +00:00
Richard Feldman
52bc5a0488
run_tests: Stop treating non-workspace dirs as packages (#60502)
The `orchestrate` job maps changed directories to root-workspace package
names, and when no mapping was found it fell back to using the raw
directory name as a package. Because `tooling/lints` is a separate
workspace (not a root-workspace member), a change under
`tooling/lints/**` produced the filter `rdeps(lints)`, which `cargo
nextest run --workspace` rejects with "operator didn't match any
packages" — failing `run_tests` for any such PR. This drops that
fallback so an unmapped directory falls through to the existing "no
package changes → run all tests" path instead.

Release Notes:

- N/A
2026-07-07 01:20:27 +00:00
Ben Kunkle
e7311d52ba
Split interpolate failure rejection reason (#60499)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
## Summary

- Added `InterpolateFailed` as a distinct edit prediction rejection
reason.
- Kept `InterpolatedEmpty` for successful interpolation that leaves no
edits.
- Updated interpolation callers to classify failed interpolation
separately from empty edit vectors.

Release Notes:

- N/A
2026-07-06 22:12:33 +00:00
Marshall Bowers
9064f26a45
cloud_api_types: Add new ID fields to AuthenticatedUser (#60497)
This PR adds the new `id_v2` and `legacy_user_id` fields to the
`AuthenticatedUser` type and starts using them in place of the `id`
field.

Release Notes:

- N/A
2026-07-06 21:27:24 +00:00
davidhi7
98fe76caad
editor: Fix completion labels not being rendered completely (#56976)
PR https://github.com/zed-industries/zed/pull/45892 changed the logic
for rendering completion labels and divided rendering up into the "main"
part (consisting of the completion's `filter_range`) and "details" part
(everything after `filter_range`), but no longer renders text that comes
before the `filter_range`. This text is probably not relevant in too
many cases but makes some Rust completions unclear and ambiguous, as can
be seen in this example:

Without this change:
<img width="1262" height="313" alt="Image"
src="https://github.com/user-attachments/assets/dcdd9da4-c449-445d-b379-771b368c3778"
/>

With this change:
<img width="1328" height="376" alt="grafik"
src="https://github.com/user-attachments/assets/05fdaf95-7a18-4d50-8e3b-35be10d9a1ef"
/>

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #56973

Release Notes:

- Fix completion labels not being rendered completely

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-06 21:22:14 +00:00
Kirill Bulatov
48c03b2f7f
Better "Restart to Update" button dismissals (#60448)
Follow-up of https://github.com/zed-industries/zed/pull/59994 deals with

<img width="173" height="39" alt="restart to update"
src="https://github.com/user-attachments/assets/5209e15b-cbf9-4530-b0da-7326c8131448"
/>

button overly appearing.

The PR mentioned fixed the redownload issue, but the title bar code
re-surfaced the button on every update check as `is_updated` in this
semantics means a new Nightly update is ready to be installed:
159246f008/crates/auto_update/src/auto_update.rs (L171-L173)
and the old code used this as "can show the button again" reason after
each recheck.

Instead, track dismissed update state better both in the title bar and
the global auto updater, so no new version checks can trigger this and
no new Zed windows get this button again (as it is now).

Release Notes:

- Improved "Restart to Update" button dismissals
2026-07-06 19:47:57 +00:00
Bennet Bo Fenner
eeff97950f
Add license to tooling/lints crate (#60468)
Seeing `script/check-license` fail because we forgot to add a license in
#58496

> Error: tooling/lints does not contain a LICENSE-GPL or LICENSE-APACHE
symlink

Release Notes:

- N/A
2026-07-06 16:10:48 +00:00
Finn Evers
283d5054ec
ci: Set and enforce more default permissions (#60222)
Release Notes:

- N/A
2026-07-06 16:02:54 +00:00
Salman Farooq
5d6c88cdb7
workspace: Use ellipsis character in "New" tooltip (#60467)
Explanation by @danilo-leal at
https://github.com/zed-industries/zed/pull/60181#issuecomment-4866118310

## Testing

Ran with `cargo run` and tooltip appears correctly.

## Release Notes:

- N/A
2026-07-06 15:56:59 +00:00
Danilo Leal
f16b46419d
Improve diff tabs toolbar design (#60464)
This PR adds a bunch of design improvements to the toolbar of (tab)
views that display diffs: the uncommitted changes, branch diff, and
agent diff tabs. This involves adjusting spacing, sizing, and other
small tweaks across all of these views, including touching up the
divider component API a little bit, adjusting Git icons design, adding
diff stat numbers to the uncommitted changes tab so its consistent with
the others, and more (e.g., moving the split buttons into its own
component to ensure they look the same across all uses). Ended up also
going on a slight de-tour to make all uses of the split button in the
Git panel consistent design-wise.

All in all, this is just tidying up the design of all of these surfaces
that are all relatively similar.

| Branch Diff | Uncommitted Diff | Single-file Diff | Git Panel |
|--------|--------|--------|--------|
| <img width="800" alt="Screenshot 2026-07-06 at 11  25@2x"
src="https://github.com/user-attachments/assets/4ebf87e7-c52d-41d9-9de3-7944125114a1"
/> | <img width="800" alt="Screenshot 2026-07-06 at 11  25 2@2x"
src="https://github.com/user-attachments/assets/51ccd2eb-904b-455a-a708-b4cb50b3a7cb"
/> | <img width="800" alt="Screenshot 2026-07-06 at 11  26@2x"
src="https://github.com/user-attachments/assets/6ae7df68-d1bd-4d36-a250-ba16e43c4e8e"
/> | <img width="800" alt="Screenshot 2026-07-06 at 11  26 2@2x"
src="https://github.com/user-attachments/assets/e032bc60-4551-41f0-b578-90ed305167c2"
/> |

Release Notes:

- Added diff stat numbers to the uncommitted changes view.
2026-07-06 15:46:44 +00:00
David Alecrim
e58a02d974
Fix text finder crash from unbounded memory use (#60377)
- Fixes #60238.
- The text finder could spike memory into the tens of gigabytes and
crash the app. The finder was copying the full matched line into every
match it kept, so memory grew with the number of matches times the size
of each line. Enough matches carrying enough text and the process runs
out of memory.

## Solution

- Stop storing line text on matches. Each match now keeps only its
position and column, and the text shown in a row is built lazily for the
visible rows, using a bounded slice around the match rather than the
whole line. Line boundaries come from the buffer's line index instead of
re-scanning the content per match.
- Cap the number of matches the finder builds while streaming results,
using the same ceiling project search already applies. This keeps the
finder from ever assembling an unbounded match list on the UI thread.
- Behavior is unchanged: every occurrence is still its own row, jumping
to a match lands on the exact line and column, and the rendered text and
highlighting look the same. The finder just no longer holds text
proportional to the match count.

## Testing
- Added tests covering the new behavior: the match count is capped while
streaming, every occurrence below the ceiling still becomes its own
match at the correct line and column, and the rendered slice around a
match stays bounded on a huge line while still covering a whole short
line.
- Verified by hand with the reproduction from the issue. The screenshot
below shows the memory usage and the matching against the issue data:
<img width="1277" height="1053" alt="Screenshot 2026-07-03 at 17 43 11"
src="https://github.com/user-attachments/assets/71c0aa3b-2423-427d-91ba-792951d9ba31"
/>

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed a crash and high memory usage in the text finder.
2026-07-06 15:21:32 +00:00
guopenghui
2b0a83ea81
picker: Give delegate the initial preview layout (#60007)
## Objective

- Provide initial preview layout information for the picker delegate.
This ensures that when match items with different styles need to be
displayed under horizontal or vertical layouts, they render correctly on
the first display.

## Solution

- Pass the initial layout information to the delegate in the picker
constructor


## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable


---

Release Notes:

- N/A
2026-07-06 15:00:36 +00:00
Yevhen Nakonechnyi
050e6f3407
text_finder: Fix crash when dismissing the finder (#60437)
Some checks are pending
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
# Objective

Fixes #60436

Pressing Escape (or triggering any workspace-level action that closes
modals, such as `pane::DeploySearch` / `search::NewSearch`) while the
Text Finder was open crashes the app with:

```
cannot read workspace::Workspace while it is already being updated
crates/gpui/src/app/entity_map.rs:164
```

`TextFinder::on_before_dismiss` read the `Workspace` entity
(`workspace.read(cx).database_id()`) to persist the last search query.
Dismissal can be initiated from inside a `workspace.register_action`
handler — e.g. `buffer_search`'s `SearchActionsRegistrar` calls
`workspace.hide_modal(...)` while the `Workspace` entity is leased — so
the modal layer invokes `on_before_dismiss` synchronously under that
lease, and the read trips GPUI's re-entrancy guard. Since the finder
seeds the last query on open, the query is non-empty immediately, making
the crash reproducible on the very first Escape.

Dump file analysis confirms the diagnosis.

## Solution

Stop reading the `Workspace` entity in the dismiss path. Both places
that create the modal (`TextFinder::open` and
`TextFinder::open_from_project_search`) already run inside
`workspace.update_in`, where `workspace.database_id()` is a plain field
access on `&mut Workspace`. Capture the `Option<WorkspaceId>` there,
store it on `TextFinder`, and have `on_before_dismiss` use the stored
id. The dismiss path now touches no entity that can be mid-update, so it
is safe regardless of which code path initiates `hide_modal`. The
remaining reads in `on_before_dismiss` (`picker` and its query editor)
are never leased in any dismissal chain.

The `id` is captured once at creation; a workspace that gains a database
id during the modal's lifetime would persist under `None` (i.e. skip
persistence), which matches the previous behavior for unpersisted
workspaces.

## Testing

- Verified the fix by code review and re-tested in a rebuilt bundle
(local build).
- To reproduce/verify: open the Text Finder (`text_finder::Toggle`),
type a query (or rely on a seeded one), then press Escape with a binding
that resolves to `editor::Cancel`, or press `cmd-shift-f`
(`DeploySearch`) while the finder is open. Before this change the app
crashes. After it, the modal dismisses and the query is persisted.
- Tested on macOS 26.5.1 (arm64); the affected code is
platform-independent.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks — no unsafe blocks
- [x] The content adheres to Zed's UI standards — no UI changes
- [x] Tests cover the new/changed behavior — added
- [x] Performance impact has been considered and is acceptable — one
`Option<WorkspaceId>` copy at modal creation

---

Release Notes:

- Fixed a crash when closing the Text Finder while a workspace action
(e.g. Escape via `editor::Cancel`, or deploying search) triggered the
dismissal
2026-07-06 13:47:21 +00:00
Tautik Agrahari
b1f4563909
docs: Add Tailwind LSP configuration section for Go (Templ) (#55255)
Document how to configure the Tailwind CSS language server for Go
(Templ).

Ref #43969.

Release Notes:

- N/A

---------

Co-authored-by: Kunall Banerjee <hey@kimchiii.space>
2026-07-06 12:19:35 +00:00
Bennet Bo Fenner
54bf918329
acp: Set agent as default when installing it from registry (#60452)
This PR makes it so that when you install an agent from the registry we
set it as the default agent. We apply the same behaviour when installing
an ACP agent from the onboarding page, which should make it less
confusing for new users (they won't see the Zed agent when they see a
new thread, but instead see the agent they installed)



https://github.com/user-attachments/assets/81f4711f-4c9a-4606-882a-9f944c90dfa3



Release Notes:

- agent: Improved onboarding experience when installing ACP agents
2026-07-06 11:37:43 +00:00
Finn Evers
0d789ded0e
extension_rollout: Allow rollout for extension-workflows tag (#60354)
Also cleans up the tag update step to instead use the GitHub API for
this.

Release Notes:

- N/A
2026-07-06 11:21:14 +00:00
Kaedin
c545fb67d0
docs: Add Tailwind CSS LSP configuration for Gleam (#58115)
Documents how to configure the Tailwind CSS language server for Gleam,
and updates the link on the Tailwind landing page to anchor at the new
section so it matches the pattern used for Astro, ERB, HEEx, HTML,
TypeScript, JavaScript, PHP, Svelte, and Vue.

The config follows what was documented in #43968 when Gleam was added to
`tailwind.rs`.

Refs: #43969

Release Notes:

- N/A

---------

Co-authored-by: Kaedin Hano-Hollis <180361443+kaedinhanohano@users.noreply.github.com>
Co-authored-by: Kunall Banerjee <hey@kimchiii.space>
2026-07-06 10:30:01 +00:00
Ben Brandt
159246f008
acp: Support boolean ACP config options (#60446)
Removing the feature flag now that the API is stable.

Release Notes:

- acp: Support boolean toggles for ACP session configuration in agents
that use them.

---------

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
2026-07-06 09:14:38 +00:00
João Soares
c4a9b1aa4b
Fix blame hover popover not showing on first trigger when inline blame is disabled (#50769)
Closes #50285

## Summary

When `inline_blame` is disabled in settings and `editor::BlameHover` is
triggered,
the blame popover fails to appear on the first invocation because
`start_git_blame`
kicks off an async task to fetch blame data, but `blame_hover`
immediately tries to
read that data synchronously before it's available.

This fix registers a one-shot observation on the blame entity when it's
freshly
created, so the popover is shown once blame data has finished
generating.

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [ ] ~Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)~
(No UI changes)

## Videos:
### Before:

https://github.com/user-attachments/assets/d2ca4f8e-8186-49af-9908-5a82bfca0de2


Ps: pressed the keybind two times

### After:

https://github.com/user-attachments/assets/08575cd9-2bbc-462d-92fa-f0e7ef23d8d6



## Release Notes:

- Fixed blame hover popover not appearing on first trigger when inline
blame is disabled (#50285)

---------

Co-authored-by: Chris Biscardi <chris@christopherbiscardi.com>
2026-07-06 08:51:35 +00:00
Bennet Bo Fenner
69664ab9d3
agent: Show errors when provider is not authenticated but model is configured (#60417)
This PR improves the onboarding experience when using the agent panel.
Previously we would pick a fallback model in case the provider failed to
authenticate/was slow to resolve models. However, this code had a race
condition (for providers that resolve models dynamically like
Anthropic/GitHub Copilot), since we pick a fallback immediately after
all providers have been authenticated. However, at that point the
configured provider might not have resolved its models, so we would fall
back to a different provider. At some point we added a workaround for
the zed.dev provider.
We landed on a much simpler approach that eliminates the race condition:
We only pick a fallback model in case the user actually has no model
configured in his settings. In case the user has a model configured, we
won't fallback and show an actionable error message:

1. Model is not set and no fallback available

<img width="862" height="78" alt="image"
src="https://github.com/user-attachments/assets/e8e7472a-1c05-4cd2-8efc-49e6d921b0a4"
/>

2. Model is set, but provider is not authenticated

<img width="865" height="65" alt="image"
src="https://github.com/user-attachments/assets/3c8cbf1d-7dd5-4b2e-809a-61e6662721a3"
/>

3. Model is set, provider is authenticated, but model is not in model
list

<img width="863" height="63" alt="image"
src="https://github.com/user-attachments/assets/b97defa2-3fb4-4ec0-b3c6-26878d1c815c"
/>

4. Model is set, but provider is not recognised

<img width="865" height="67" alt="image"
src="https://github.com/user-attachments/assets/ecff1e3f-4f6b-47eb-a25d-125a104baafc"
/>



This plays well with the reason why we have the fallback model in the
first place: We only want to pick a fallback for users that open Zed for
a first time and e.g. have an Anthropic API key present in their
environment. As soon as the user manually changes his provider/model we
won't apply the fallback anymore.



Release Notes:

- agent: Improved error messaging when provider is not configured
- agent: Improve fallback model selection

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
2026-07-06 08:18:50 +00:00
XiaoYan Li
01568e5569
Swap Kotlin LSPs in documentation to reflect actual default config (#54061)
## Summary

- The default settings in `assets/settings/default.json` disable
`kotlin-language-server` (prefixed with `!`) and enable `kotlin-lsp` as
the primary LSP, but the Kotlin documentation listed them the other way
around. This swaps the order so the docs match reality.

## Test plan

- [x] Verified `assets/settings/default.json` has
`["!kotlin-language-server", "kotlin-lsp", "..."]` for Kotlin
- [x] Confirmed the documentation now lists `kotlin-lsp` as the primary
Language Server and `kotlin-language-server` as the alternate

Release Notes:

- N/A

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-06 08:10:13 +00:00
Sathwik Chirivelli
62477092a2
git_panel: Scope folder expansion state to sections (#60396)
# Objective

Prevent collapsing a folder in one Git panel section from also
collapsing the same folder path in other sections.

## Solution

Key directory expansion state by `TreeKey`, which includes both the
section and repository path, instead of by `RepoPath` alone. This also
keeps persisted tree state, stale-state cleanup, and selected-file
reveal behavior section-aware.

## Testing

- Added `test_tree_view_directory_expansion_is_scoped_to_section` to
cover the same folder path under Tracked and Untracked independently.
- `cargo fmt -p git_ui -- --check`
- `git diff --check`
- Attempted `cargo test -p git_ui
test_tree_view_directory_expansion_is_scoped_to_section --lib`, but
dependency compilation could not complete because the local disk ran out
of space. Reviewers can run that command to execute the focused
regression test.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Suggested .rules additions

- In the Git panel tree view, key per-row UI state by `TreeKey` rather
than `RepoPath`, because the same directory can appear in multiple
sections.

---

Release Notes:

- Fixed collapsing a folder in one Git panel section also collapsing it
in other sections.
2026-07-06 06:54:29 +00:00
Anthony Gregis
24c5b37e6e
Filter AI keybindings when AI features are disabled (#56936)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
When `disable_ai` is set, bindings to AI-namespaced actions (assistant,
agent, inline_assistant, acp, edit_prediction, zeta) are now dropped at
keymap load time. Previously the bindings stayed active and their
handlers silently no-op'd, which shadowed lower-precedence editor
defaults — pressing ctrl-enter in an editor was captured by
`assistant::InlineAssist` and did nothing instead of falling through to
`editor::NewlineBelow`.

The filter applies to both default and user bindings, and a settings
observer reloads the keymap when `disable_ai` toggles at runtime.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #56692

Release Notes:

- Fixed AI keybindings remaining active and shadowing editor defaults
when AI features are disabled

---------

Co-authored-by: Chris Biscardi <chris@christopherbiscardi.com>
2026-07-06 05:49:03 +00:00
Eagl61
04de6dab7c
Show type-changed files in commit diffs (#60422)
# Objective

Zed ignores files marked by Git as type-changed (T), causing commits
containing only these changes to show 0 Changed Files.

For example, commit d7cc949e61 changes
crates/eval_utils/LICENSE-GPL from a regular file to a symlink, but Zed
displays no changes.

  ## Solution

Handle TypeChanged files like modified files by loading both their old
and new contents.

  ## Testing

  - Added parser coverage for the T status.
  - Added a repository test for a regular-file-to-symlink change.
  - Ran cargo test -p git.
  - Ran ./script/clippy -p git.
  - Manually verified the example commit on macOS.

  ## Self-Review Checklist:
  - [x] I've reviewed my own diff for quality, security, and reliability
  - [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and [icon](https://github.com/zed-
  industries/zed/blob/main/crates/icons/README.md) guidelines)
  - [x] Tests cover the new/changed behavior
  - [x] Performance impact has been considered and is acceptable

  ## Showcase

Before:
<img width="200" alt="image"
src="https://github.com/user-attachments/assets/277c5938-5c2c-47b7-902d-9061a14c062a"
/>

After:
<img width="200" alt="image"
src="https://github.com/user-attachments/assets/f99d0005-0712-4843-814e-d73b11f64782"
/>

  ———

  Release Notes:

- Fixed type-changed files not appearing in Git Graph and commit views.
2026-07-06 03:15:43 +00:00
Remco Smits
b4d9194fce
themes: Don't try to call fs.load_bytes on a directory path on rescans (#60399)
# Objective

Before this change, we watched the themes directory itself. During
rescans, this could generate events for the directory, causing Zed to
attempt to load `event.path` as a file via
`fs.load_bytes(&event.path).await.log_err()`. This happened because
`fs.metadata(&event.path).await.ok().flatten().is_some()` also returns
true for directories.

Since load_bytes will always fail for a directory, we can avoid the
unnecessary call by skipping it whenever `event.path` refers to a
directory.

The rescan happens when Zed lost sync with the filesystem. That will
produce the following logs:

```
2026-07-01T16:03:35+02:00 WARN  [fs::fs_watcher] filesystem watcher lost sync for many files, not logging more
2026-07-01T16:03:35+02:00 ERROR [crates/zed/src/main.rs:1891] Is a directory (os error 21)
```

## Solution

The solution is that we just can check if the `event.path` is a
directory if so we can skip the
`fs.load_bytes(&event.path).await.log_err()` part described aboth.

## Testing

All tests should still pass and there should be no impact since this
only happens on rescans when the system is under pressure with alot of
fs events.

**Note**: No regression test was added since there's nothing to test
here.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- N/A
2026-07-06 03:12:43 +00:00
Danny Clarke
5b805ac074
git_graph: Selectable commit message in detail panel (#59674)
# Objective

Fixes #57766 

Allow users to select and copy the text of commit messages from the git
graph details panel.

## Solution

Implements selectable commit message in the detail panel using
`MarkdownElement`s a la `commit_view`.

### Affordances considered and abandoned:

- "copy message" and/or "copy subject" buttons, but the UI here is
already pretty tight and I don't feel confident enough in my design
chops or familiarity with Zed to propose bigger UI changes here.

- Default-collapsed message with expand button, a la `commit_view`. The
use-case of this panel is to get the gist of a commit while moving
somewhat quickly through the graph. One might argue that the subject
line alone is enough for that, but often it isn't and the thought of
having to constantly expand commit messages rather than making the
message immediately visible with scroll felt like it would produce a
repetitious experience.

## Testing

- Adds two unit tests to verify that selection works and is idempotent

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

Video demonstrating the functionality:

https://github.com/user-attachments/assets/b910aaa3-2db2-4da8-904b-21b270677ca9

---

Release Notes:

- Show full commit message as markdown in details panel of the git graph
2026-07-06 01:58:57 +00:00
di404
6e9ff7a4f3
Fix agent hyperlinks do not open files (#56283)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
## Summary

Fix agent message hyperlinks that point to Windows file paths but are
not parsed as openable project paths.

This covers links like:

```md
[Cargo.toml](</C:/Projects/Example Workspace/Cargo.toml:2>)
[filename.ext](C:\Projects\Example%20Workspace\path\to\filename.ext:42)
[AGENTS.md](</c/Projects/Example Workspace/AGENTS.md>)
```

## Problem

Agent responses can emit Markdown hyperlinks whose targets are Windows
paths rather than `file://` URLs. Some of those targets include a
leading slash before the drive (`/C:/...`), Git Bash/MSYS-style drive
prefixes (`/c/...`), percent-escaped spaces, or line suffixes. These
were not normalized before mention parsing, so clicking the hyperlink
could do nothing instead of opening the file.

## Solution

- Normalize hyperlink path targets before parsing them as `MentionUri`
paths.
- Decode percent escapes in bare path targets so `%20` becomes a literal
space before path/line parsing.
- Convert Windows-compatible hyperlink paths such as `/C:/...` and
`/c/...` into native Windows paths.
- Generate file resource links from `find_path_tool` through
`MentionUri::to_uri()` instead of hand-building `file://` strings.

## Result

Before: clicking agent path hyperlinks did not open the referenced file.

After:  


https://github.com/user-attachments/assets/6c7fad77-4a1e-4497-a4f9-4a4fdf86d527


## Validation

- `cargo test -p acp_thread test_parse_windows --features test-support`
- `cargo fmt --check`

## Follow-up changes

Additions on top of the original work above:

- Moved the hyperlink heuristics into a dedicated
`MentionUri::parse_hyperlink` entry point. `MentionUri::parse` stays
strict, so canonical mention URIs round-trip verbatim and other callers
(message editor, thread deserialization, resource links) are unaffected.
- Percent escapes that decode to path separators (`%2F`, `%5C`) are left
encoded, so decoding can never change which directories a path
traverses.
- Bare paths with escapes are ambiguous (a file may literally be named
`a%20b.rs`): `open_link` prefers the decoded interpretation and falls
back to `MentionUri::parse_hyperlink_literal` when the decoded path
doesn't resolve in the project but the literal one does.
- Links to files outside the project's worktrees now open, gated by an
async existence check through the project `Fs` (correct for remote
projects; broken links no longer create empty buffers or add worktrees).
`open_link` and the mention-crease open path are unified into one
`open_abs_path_at_point`, which now also places the cursor for
out-of-project selection/symbol links.
- `grep_tool` resource links also go through `MentionUri::to_uri()` now,
fixing malformed `file://C:\...` URIs and unencoded spaces.
- Added tests: percent-escape disambiguation, out-of-project link
opening, drive-letter normalization, UNC paths, and literal-path parsing
(`cargo test -p acp_thread mention`, `cargo test -p agent_ui open_link`,
`cargo test -p agent grep_tool`); manually verified the link spellings
above on Windows.

Release Notes:

- Fixed agent path hyperlinks on Windows when paths contain spaces or
shell-style drive prefixes.

---------

Co-authored-by: Martin Ye <martin@zed.dev>
2026-07-05 03:54:54 +00:00
David Irvine
e3b73c6b30
bedrock: Fix Claude Sonnet 5 and Fable 5 routing outside US regions (#60378)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Follow-up to #60360 and #59016 (cc @NeelChotai @bennetbo).

Claude Sonnet 5 and Claude Fable 5 cannot be invoked with on-demand
throughput — AWS requires an inference profile, and only `us.*` and
`global.*` profiles exist for these models ([Sonnet 5 model
card](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-5.html),
[Fable 5 model
card](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-fable-5.html)).

For users in EU/APAC regions without `allow_global` enabled, Zed
generated profile IDs like `eu.anthropic.claude-sonnet-5`, which fail
with:

```
ResourceNotFoundException: Model not found.
```

And falling back to the bare model ID isn't an option either, since
direct invocation fails with:

```
Invocation of model ID anthropic.claude-fable-5 with on-demand throughput isn't supported.
Retry your request with the ID or ARN of an inference profile that contains this model.
```

Confirmed against the live AWS API — only `us.*` and `global.*` profiles
exist:

```
❯ aws bedrock list-inference-profiles --region eu-west-1 \
    --query "inferenceProfileSummaries[?contains(inferenceProfileId, 'sonnet-5') || contains(inferenceProfileId, 'fable')].[inferenceProfileId,status]" \
    --output table
------------------------------------------------
|             ListInferenceProfiles            |
+------------------------------------+---------+
|  global.anthropic.claude-sonnet-5  |  ACTIVE |
|  global.anthropic.claude-fable-5   |  ACTIVE |
+------------------------------------+---------+
```

This change removes both models from the EU match arm and routes them
through the global inference profile when no regional profile is
available. US regions continue to use the `us.*` profile. Verified
working from `eu-west-1`.

Note: this means requests from non-US regions route through the global
profile, which may dispatch inference to any supported AWS region.
That's inherent to how AWS exposes these models — there is no
EU-resident option today.

Release Notes:

- Fixed Claude Sonnet 5 and Claude Fable 5 failing on Amazon Bedrock in
non-US regions.

---------

Co-authored-by: Neel <neel@zed.dev>
2026-07-04 12:15:01 +00:00
Danilo Leal
d97bbf3369
agent_ui: Use a callout for the sandbox warning (#60386)
Just a small tweak to use an existing component where the UI is a bit
tidier.

Release Notes:

- N/A
2026-07-04 09:15:51 +00:00
Kirill Bulatov
1a99eba192
Do not redownload same Nightly updates over and over (#59994)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
If one gets a Nightly update but does not restart and rather dismisses
the notification, the next update check will do the update again.

This is caused by the fact that the version check is done based on
semver, but Nightly has the very same version that differs by the SHA
only.
Adjust the Nightly update check to skip re-downloads if SHA parts of the
version match also.

https://github.com/zed-industries/zed/pull/59852 does more changes, this
PR extracts the part I am sure about.

Release Notes:

- N/A
2026-07-03 23:29:01 +00:00
Miguel Raz Guzmán Macedo
4b7369481d
Add dylint lint library for Zed-specific patterns (#58496)
Adds a dylint library under tooling/lints that flags Zed-specific
anti-patterns:

* shared_string_from_str_literal, 
* async_block_without_await, 
* entity_update_in_render, 
* notify_in_render, 
* owned_string_into_shared, 
* len_in_loop_condition, and 
* blocking_io_on_foreground. 

Includes UI tests, a single-lint helper, and workspace.metadata.dylint
registration so cargo dylint --all discovers it. The library pins its
own nightly toolchain (kept out of the main workspace) and tracks dylint
6.

Release Notes:

- N/A

Self-Review Checklist:

- [ ] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable

Closes #ISSUE

Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-03 22:05:34 +00:00
Lukas Geiger
b77ec90b2e
git: Do not recompute git_access on every file change (#59521)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
# Objective

As mentioned in #59514, the git panel currently runs a full `git status`
for checking whether `git` has access to the repository. This was
introduced in #43693 and currently runs on every file save which is
problematic for large repos where `git status` runs a lot of
computation.

## Solution

This PR caches the `git_access` on the git panel such that we don't need
re-check for git access on every file save.

## Testing

I manually verified that the git panel doesn't trigger anymore
unnecessary git status commands.

**main:**
```
2026-06-18T00:04:24+01:00 DEBUG [project::git_store] start recalculating diffs for buffer 4294967643
2026-06-18T00:04:24+01:00 DEBUG [project::git_store] finished recalculating diffs for buffer 4294967643
2026-06-18T00:04:24+01:00 DEBUG [project::git_store] start recalculating diffs for buffer 4294967643
2026-06-18T00:04:24+01:00 DEBUG [project::git_store] finished recalculating diffs for buffer 4294967643
2026-06-18T00:04:24+01:00 DEBUG [project.format.local] formatting buffer '"/Users/lgeiger/code/zed/CONTRIBUTING.md"'
2026-06-18T00:04:24+01:00 DEBUG [project.format.local] no changes made while formatting
2026-06-18T00:04:24+01:00 DEBUG [git::repository] Checking for git status in ["CONTRIBUTING.md"]
2026-06-18T00:04:25+01:00 DEBUG [git::repository] Checking for git status in []
```
```
/opt/homebrew/bin/git -c core.fsmonitor=false -c log.showSignature=false --no-optional-locks --no-pager status --porcelain=v1 --untracked-files=all --no-renames -z -- CONTRIBUTING.md
/opt/homebrew/bin/git -c core.fsmonitor=false -c log.showSignature=false --no-optional-locks --no-pager diff --numstat --no-renames HEAD -- CONTRIBUTING.md
/opt/homebrew/bin/git -c core.fsmonitor=false -c log.showSignature=false --no-optional-locks --no-pager config --get commit.template
/opt/homebrew/bin/git -c core.fsmonitor=false -c log.showSignature=false --no-optional-locks --no-pager status --porcelain=v1 --untracked-files=all --no-renames -z --
```

**This PR:**
```
2026-06-18T02:47:54+01:00 DEBUG [project::git_store] start recalculating diffs for buffer 4294967656
2026-06-18T02:47:54+01:00 DEBUG [project::git_store] finished recalculating diffs for buffer 4294967656
2026-06-18T02:47:55+01:00 DEBUG [project.format.local] formatting buffer '"/Users/lgeiger/code/zed/CONTRIBUTING.md"'
2026-06-18T02:47:55+01:00 DEBUG [project.format.local] no changes made while formatting
2026-06-18T02:47:55+01:00 DEBUG [git::repository] Checking for git status in ["CONTRIBUTING.md"]
```
```
/opt/homebrew/bin/git -c core.fsmonitor=false -c log.showSignature=false --no-optional-locks --no-pager diff --numstat --no-renames HEAD -- CONTRIBUTING.md
/opt/homebrew/bin/git -c core.fsmonitor=false -c log.showSignature=false --no-optional-locks --no-pager status --porcelain=v1 --untracked-files=all --no-renames -z -- CONTRIBUTING.md
/opt/homebrew/bin/git -c core.fsmonitor=false -c log.showSignature=false --no-optional-locks --no-pager config --get commit.template
```


## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

/cc @dinocosta


---

Release Notes:

- Improved Git Panel performance by no longer re-checking repository
access on every file change

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
2026-07-03 20:43:32 +00:00
Gavin Luo
c56646ffdf
terminal: Fix IME candidate window not following cursor in TUI apps (#59911)
# Objective

Fix IME candidate window not following cursor position in the integrated
terminal when running fullscreen TUI applications like opencode.

When using IME (Input Method Editor) in Zed's terminal with fullscreen
TUI applications (e.g., opencode), the candidate window does not follow
the cursor position. This issue does not occur in normal terminal usage
(e.g., bash shell).

# Solution

The root cause was three-layer blocking preventing IME position updates
in ALT_SCREEN mode:

1. **ALT_SCREEN blocking**: `selected_text_range()` returned `None` in
ALT_SCREEN mode, causing `selected_bounds()` to return `None`
2. **Missing trigger**: `Event::Wakeup` did not call
`invalidate_character_coordinates()`, so cursor movement did not trigger
IME position updates
3. **Composition blocking**: `update_ime_position()` skipped updates
when `state.composing` was true

Fixes:
- Remove ALT_SCREEN check in `selected_text_range()` so IME position
updates work in fullscreen TUI apps
- Add `window.invalidate_character_coordinates()` in `Event::Wakeup` to
trigger IME position updates when terminal cursor moves
- Remove `state.composing` check in `update_ime_position()` to allow IME
position updates during text-input-v3 composition
- Remove unused `terminal` field from `TerminalInputHandler` struct

# Testing

**Did you test these changes? If so, how?**
- Yes, tested on Linux GNOME Wayland with iBus input method
- Verified IME candidate window correctly follows cursor in opencode
- Verified normal terminal usage with IME still works correctly

**Are there any parts that need more testing?**
- Other IMEs (fcitx, etc.) may need testing

**How can other people (reviewers) test your changes?**
1. Open Zed's integrated terminal
2. Run a fullscreen TUI application like `opencode`
3. Activate IME (e.g., iBus with Chinese input)
4. Type text and observe the IME candidate window follows the cursor

**What platforms did you test these changes on?**
- Linux (GNOME Wayland) - tested
- macOS - not tested (may have different IME behavior)
- Windows - not tested (different code path)

# Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content adheres to Zed's UI standards (UX/UI and icon
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

# Showcase

<details>
  <summary>Before</summary>
<video
src="https://github.com/user-attachments/assets/ee2fac4e-801b-49af-a57e-32ce25e01db5"
width="320" height="180" />
</details>

<details>
  <summary>After</summary>
<video
src="https://github.com/user-attachments/assets/c669ea99-2ffc-4179-b587-a47873e33e70"
width="320" height="180" />
</details>

---

Release Notes:

- Fixed IME candidate window not following cursor in terminal TUI apps
2026-07-03 20:25:16 +00:00
Artin
5aa6e8a0b3
Reduce OAuth scope to avoid Windows credential size limit (#58541)
I encountered an issue where I couldn't sign in to ChatGPT Subscription
on Windows:

```
ChatGPT subscription sign-in failed to persist credentials: 
Failed to write credentials to Windows Credential Manager: 
占位程序接收到错误数据。 (0x800706F7)
```

Interestingly, only one of my three ChatGPT accounts had this problem —
the other two signed in successfully.

## What I Found

After investigating, I noticed that the OAuth scope in
`openai_subscribed.rs` requests 6 permissions:

```
openid profile email offline_access api.connectors.read api.connectors.invoke
```

I wrote a test script to measure token sizes and found that:
- With 6 scopes: my problematic account's token was **2578 bytes**
- With 4 scopes (removing `api.connectors.read` and
`api.connectors.invoke`): the same token was **2516 bytes**

Since Windows Credential Manager has a 2560-byte limit
(`CRED_MAX_CREDENTIAL_BLOB_SIZE`), this could explain why some accounts
fail while others succeed — it depends on the base token size.

## My Hypothesis

The issue seems to be that:
- Error code `0x800706F7` = `RPC_X_BAD_STUB_DATA` (size limit, not
format)
- Accounts with larger base tokens exceed the limit when the extra 48
chars are added
- Accounts with smaller base tokens still fit, which is why this isn't
universally reproducible

I'm not 100% certain this is the root cause, but the evidence seems to
point in this direction.

## Proposed Fix

I removed `api.connectors.read` and `api.connectors.invoke` from the
OAuth scope, since:
- These scopes don't appear to be used by Zed's current ChatGPT
integration
- OpenCode (another tool using the same OAuth provider) successfully
uses only 4 scopes
- This fix allowed my problematic account to sign in

However, I'd like to ask the maintainers:
- Are there plans to use OpenAI Connectors API in the future?
- Are there security or compliance reasons for requesting these extra
scopes?

If the answer is yes to either, we'd need a different approach (e.g.,
splitting credentials, using encrypted file storage, or checking token
size before storage).

## Testing

- [x] Verified my problematic account now signs in successfully
- [x] Verified my other accounts still work
- [x] Ran `cargo check -p language_models` (compilation successful)
- [ ] Would appreciate help testing on macOS/Linux

## Questions

1. Does this analysis make sense?
2. Are there any other considerations I'm missing?
3. Would it be helpful to add a size check with a clearer error message
for future cases?

I'm happy to adjust this approach based on your feedback.

Release Notes:

- Fixed ChatGPT Subscription sign-in failing on Windows for some
accounts by removing unused OAuth scopes (`api.connectors.read`,
`api.connectors.invoke`) that pushed JWT tokens over Windows Credential
Manager's limit.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-03 19:38:42 +00:00
Tautik Agrahari
ea3d0f7abe
keymap: Avoid format-vs-rules collision in JetBrains overlay (#55364)
the jetbrains overlay binds `ctrl-alt-l` (linux) / `cmd-alt-l` (macos)
to `editor::Format` (jetbrains "reformat code"), which collides with the
default `agent::OpenRulesLibrary` binding. the agent panel shows the
conflicting key as the rules tooltip but pressing it formats the buffer
instead of opening rules. mirrors the windows keymap by switching the
linux/macos overlays to `shift-alt-l` in the AgentPanel context (with
the colliding key explicitly null-ed), so the hint and the action stay
in sync.

closes #49764.

Release Notes:

- Fixed the JetBrains keymap so the agent panel "Rules" entry uses
`shift-alt-l` and no longer flickers a non-functional `ctrl-alt-l` /
`cmd-alt-l` shortcut.
2026-07-03 18:54:42 +00:00
Yara 🏳️‍⚧️
5a823cf70e
docs: Add new text finder (#60189)
Mention the text finder in the docs. 

Release Notes:

- N/A

---------

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
2026-07-03 18:46:05 +00:00
Ben Brandt
fa8540ff62
Update Wasmtime dependencies (#60341)
Brings in the latest LTS security and bug fixes

Release Notes:

- N/A
2026-07-03 18:44:17 +00:00
KyleBarton
9eb8a7c0ad
Handle cases where Dockerfile aliases are chained (#57552)
Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #56189

Release Notes:

- Fixed bug to handle dev container Dockerfiles with chained aliases

---------

Co-authored-by: Martin Ye <martin@zed.dev>
2026-07-03 18:37:06 +00:00
Zak Nesler
2f1caadd38
Fix expanded commit editor (#60368)
This is a simple follow-up to #60331, the commit message in the expanded
view got messed up and was being shrunken and centered instead of taking
up the full height of the git panel.

All it needed was a `.h_full()`:

<details>
  <summary>Click to view showcase</summary>

### Before
<img width="652" height="758"
alt="file-6d7f3fd7a5de8cff5ea44c0dfbaf0ac5"
src="https://github.com/user-attachments/assets/85dca75c-7528-4876-9154-0145272c5029"
/>

### After
<img width="662" height="761"
alt="file-268420a52e7b967fc09d10007d6aa1e5"
src="https://github.com/user-attachments/assets/f39ea70b-d0a9-40e6-bb20-20cd3508ad66"
/>

</details>

@danilo-leal 

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] ~~Tests cover the new/changed behavior~~
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- N/A
2026-07-03 18:26:14 +00:00
bgfraser
12e1e24434
Initiate MCP OAuth flow on post-initialize 401 responses (#60236)
MCP servers may accept `initialize` unauthenticated but return `401`
with a `WWW-Authenticate` challenge only on a later request such as
`tools/list` or `tools/call`. Previously Zed only started the Auth flow
when `initialize` itself was challenged, so these servers failed
opaquely and stayed stuck in `Running` with a dead client.

`ContextServerStore` now handles `TransportError::AuthRequired` returned
from any request, not just startup: it runs Auth discovery and
transitions the server into `AuthRequired` so the UI offers to
authenticate. The discovery logic is shared with the startup path, and
the tool/prompt request sites route their errors through it.

Release Notes:

- Fixed MCP servers that require auth only on tool calls (not on
`initialize`) failing to prompt for authentication.

---------

Co-authored-by: Tom Houlé <tom@tomhoule.com>
2026-07-03 18:24:54 +00:00
Anthony Eid
262fb9ba2a
Show per-file diff stat in multibuffer headers (#60299)
Renders the per-file added/removed line counts (already tracked per
buffer by the multibuffer) in each diff multibuffer header, matching the
overall stat shown in the branch diff toolbar.

### Before
<img width="1624" height="1061" alt="Screenshot 2026-07-02 at 11 50 42
AM"
src="https://github.com/user-attachments/assets/3e46a90c-a4e1-4b72-8ec9-80931a7e4a9a"
/>

### After
<img width="1624" height="1061" alt="Screenshot 2026-07-02 at 12 03 58
PM"
src="https://github.com/user-attachments/assets/25900790-82f5-44e9-9a3b-731a3090f23e"
/>


Release Notes:

- Improved diff multibuffer headers to show per-file added/removed line
counts

---------

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
2026-07-03 17:55:24 +00:00
Yi Liu
6eaad52c29
language_models: Avoid sending Bedrock cache-point-only messages (#59436)
When prompt caching is enabled, `into_bedrock` pushes a `CachePoint`
block
onto a message's content whenever `message.cache && supports_caching` is
true.
This push happens before the `if bedrock_message_content.is_empty() {
continue; }`
guard. As a result, a message whose content filters down to empty (for
example,
a message that contained only content stripped during conversion) still
gets
appended to the request carrying nothing but a `cachePoint`. Bedrock
rejects such
a message with a `ValidationException`, breaking the whole request.

The Anthropic path is already internally consistent here: in
`crates/language_models/src/provider/anthropic.rs` (around the message
assembly
in `completion.rs`) the empty-content check runs first, so an empty
message is
dropped before any cache marker is attached. The Bedrock path should
behave the
same way.

This change gates the `CachePoint` push on non-empty content
(`&& !bedrock_message_content.is_empty()`), so an empty message is left
empty and
then skipped by the existing `continue`, exactly mirroring the Anthropic
ordering.
The fix is minimal and touches only the cache-point condition.

Release Notes:

- Fixed Bedrock requests failing with ValidationException when the last
message filtered to empty content while prompt caching was enabled.

---------

Signed-off-by: Yi LIU <yi@quantstamp.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-03 16:41:58 +00:00
Neel
814b152a0f
bedrock: Add Claude Sonnet 5 (#60360)
Also orders the Claude models from most to least capable.

Release Notes:

- Added Sonnet 5 to Bedrock provider

Co-authored-by: David Irvine <aviddiviner@gmail.com>
2026-07-03 15:26:15 +00:00
Hamza Paracha
53552b29a4
docs: Clarify agent notification placement (#54032)
## Summary
- describe agent notifications as OS desktop notifications instead of
claiming a fixed screen position
- keep the agent panel docs accurate across platforms and desktop
environments

Fixes #53588

Release Notes:

- N/A

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-03 15:21:52 +00:00
Jakub Konka
f4364d870e
gpui_linux: Add support for open_window in headless client (#60359)
This actually makes it possible for Linux headless client to have
working windows for computation (well, we are in headless mode after
all). This also matches macOS headless client behaviour.

Release Notes:

- N/A
2026-07-03 15:20:09 +00:00
Zak Nesler
aabcd71690
Improve commit container at larger font sizes (#60331)
At larger font sizes (e.g. I have profiles that increase the font sizes
when screen sharing) the commit button in the git panel editor does not
stay aligned to the bottom-right of the screen, and overlaps the other
elements:

<details>
  <summary>Click to show showcase</summary>


https://github.com/user-attachments/assets/e73034db-07a6-42d7-b993-9d8091e50b70

Notice the extra clipping of the commit message editor element:

<img width="500" alt="image"
src="https://github.com/user-attachments/assets/67d59405-57e2-4547-87c2-1c4728515c26"
/>

</details>

This was happening because the floating icons on the right and the
footer containing the commit button were positioned absolutely, used
pixels instead of responsive rems, and had an offset that was calculated
and did not account for changes in user rem size.

So I've moved things around to be more "flexy"... basically just
removing the absolute positioning and making the elements flush against
one another.

## Testing

I have tested this manually on macOS 27 beta 2 and Windows 11, both
using various light/dark themes, window sizes, commit text, etc.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] ~~Tests cover the new/changed behavior~~
- [x] Performance impact has been considered and is acceptable

## Showcase

Here's a demo at different zoom levels, with and without text, when
expanding the commit window, etc.

<details>
  <summary>Click to view showcase</summary>


https://github.com/user-attachments/assets/bea82da1-0c7f-4d51-a73b-d4bc4581a2b7

And here's another demo with debug colors for the different elements to
see what's going on:


https://github.com/user-attachments/assets/12fd3e95-5089-4e34-a84c-c352219ee197

</details>
---

Release Notes:

- Git Panel: Fixed overlapping elements in commit message container

---------

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2026-07-03 15:19:49 +00:00
Edvard Høiby
b497e2024a
Add Claude Fable 5 to Amazon Bedrock (#59016)
Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

What:
<img width="644" height="445" alt="image"
src="https://github.com/user-attachments/assets/b1b01f7d-8ee9-448c-a8c7-ae80c16326e4"
/>

Tested towards EU region off AWS. Makes Claude Fabel 5 accessible
through bedrock.
To enable Fable you need to opt in to `provider_data_share` in AWS.
[AWS
Docs](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-fable-5.html)

Release Notes:

- Added Claude Fable 5 to the Amazon Bedrock provider.

---------

Co-authored-by: Neel <neel@zed.dev>
2026-07-03 15:10:39 +00:00
María Craig
4da73e1970
git_ui: Add configure and docs links to commit message tooltip (#60357)
When no LLM provider is configured, hovering the disabled "Generate
Commit Message" button in the git panel/commit modal previously showed a
plain, non-interactive tooltip with no way to act on it.

This tooltip now includes two links:
- **Configure Provider** — jumps directly to the "LLM Providers"
settings sub-page.
- **See Docs** — opens `https://zed.dev/docs/ai/llm-providers`.

Also adds `IconButton::hoverable_tooltip`, mirroring the existing
`.tooltip(...)` forwarding, since `IconButton` didn't previously expose
the underlying `ButtonLike::hoverable_tooltip` capability needed for
interactive tooltip content.

Release Notes:

- Improved the commit message tooltip to link directly to LLM provider
settings and documentation when no provider is configured.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2026-07-03 14:56:37 +00:00
di404
be7e5b0338
terminal_view: Show terminal inline assist keybinding in tooltip (#55903)
## Summary

- Fixed the terminal inline assist tab bar tooltip so it resolves the
keybinding from the active terminal view.
- The bug happened when the initial terminal was closed and a new
terminal was opened: the tab bar button kept a cached focus handle for
the old terminal, so tooltip keybinding lookup could no longer find the
terminal key context.
- The button is now created while rendering the tab bar with the current
terminal view's focus handle, avoiding stale focus handles as terminals
are closed and recreated.

## Validation

- `cargo fmt --package terminal_view --check`
- `cargo check -p terminal_view --message-format short`

Release Notes:

- Fixed the terminal inline assist toolbar tooltip not showing its
keybinding after reopening terminals.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-03 13:48:03 +00:00
Maja Backman
7ed553b391
acp_thread: Shrink ACP terminal scrollback to used on exit (#60019)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
# Objective

ACP agents (Cursor, Claude Code, Codex, etc.) start a display-only
`terminal::Terminal` for each bash tool call. After the call exits, the
terminal stays alive so that its output stays visible. But the
`alacritty` `Grid` keeps a `Storage` cache that's never reclaimed.
Across a long session this memory adds up.

Partially fixes #57099

## Solution

Added `Terminal::shrink_to_used`, called from the `Exit` branch of
`on_terminal_provider_event`. This calls `Grid::truncate()` on the
alacritty grid, shrinking the `Storage` cache portion while leaving
user-visible scrollback.

## Testing

- `cargo nextest run -p terminal
shrink_to_used_preserves_user_visible_scrollback` passes — exercises the
path with >10K rows of scrollback.
- `./script/clippy` clean.
- Manually verified: ran a noisy bash tool call via an ACP agent,
observed exit, then scrolled back through the output, no regression.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Improved memory usage of ACP terminals after the tool call exits

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2026-07-03 11:41:48 +00:00
august
9448417157
project_symbols: Add preview to project symbols picker (#59863)
Candidate fix for #59822.

Adds a live preview pane to the project symbols picker, matching the
file finder and project search pickers. When the selection moves, the
selected symbol's file is shown in the preview with its declaration line
highlighted and vertically centered.

## Changes
- `project_symbols`: construct the picker with
`uniform_list_with_preview`; asynchronously open the buffer for the
selected symbol (cached by candidate id, cleared on each new query) and
implement `try_get_preview_data_for_match` to return the buffer plus the
symbol's anchor range.
- `picker`: add a public `refresh_preview` so a delegate can push
preview data once a buffer finishes opening asynchronously (the symbol
buffer is not available synchronously, unlike text search which already
holds open buffers).


#### Before
<img width="1725" height="712" alt="image"
src="https://github.com/user-attachments/assets/21dcd7d6-7cc4-4de8-ab7d-2f1ce143e814"
/>

##### After
<img width="1725" height="712" alt="image"
src="https://github.com/user-attachments/assets/db981db8-0a61-43bc-9838-c01b7bdbbf78"
/>

Can turn preview pane off by clicking at the bottom button!

## AI disclosure
AI was used for understanding the codebase and formatting this PR.

Release Notes:

- Added a preview pane to the project symbols picker

---------

Co-authored-by: Yara <git@yara.blue>
2026-07-03 10:48:15 +00:00
Ben Brandt
616b76cd59
Update Danger to 13.0.8 (#60346)
Release Notes:

- N/A
2026-07-03 10:10:41 +00:00
Richard Feldman
59185f5a70
livekit_api: Fix LiveKit token revocation timestamps (#60157)
LiveKit Cloud rejects room-join tokens as revoked when their `nbf`
predates a participant revocation. Zed generated those LiveKit JWTs with
`nbf: 0`, so a fresh participant token minted after stale connection
cleanup could still appear older than the cleanup and leave a user
joined at the collab layer without audio or screen sharing. This sets
`nbf` to the issuance time for room-join tokens while leaving admin/API
tokens unchanged, and adds regression coverage at the token, mock
LiveKit, and channel rejoin layers.

Closes FR-83

Release Notes:

- Fixed calls getting stuck without audio or screen sharing after
restarting Zed and rejoining a channel.
2026-07-03 09:50:42 +00:00
Ben Brandt
98ddc3ae2e
Update openssl dependencies (#60342)
Release Notes:

- N/A
2026-07-03 09:38:07 +00:00
Finn Evers
a91c8aa7d7
Remove more storybook leftovers (#60337)
Release Notes:

- N/A
2026-07-03 08:27:48 +00:00
Chiel Robben
4a3b763518
Add progress bar to "Downloading Zed Update..." button (#60294)
# Objective

On a few occasions I have been wondering if the Zed update download had
stalled, when in fact it's just taking a while. This PR adds a progress
bar to the "Downloading Zed Update..." button to give users feedback
that the download is indeed progressing.

## Solution

Add a custom progress bar inside the button. I tried reusing the
existing ProgressBar component but that has a fixed height of 8px which
caused the button to become taller.

## Testing

- Tested manually using the command palette `collab: simulate update
available` and stepping through the different UI states.
- Added unit test

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

<img width="1350" height="748" alt="Image 02-07-2026 at 17 23"
src="https://github.com/user-attachments/assets/ed23c97a-01b8-446b-9693-0c53a69b419d"
/>

Release Notes:

- Added a download progress indicator to the update button.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-03 08:23:06 +00:00
Kirill Bulatov
f961889ad4
Fix excluded language servers starting nonetheless (#60000)
Despite the default Zed settings containing an exclusion for
`typescript-language-server`


632dcae287/assets/settings/default.json (L2372-L2377)

it actually starts every time I open the `*.ts` file which is wrong.

Seems that the settings merging malfunctions hence the fix, but I have
some doubts as I do not recall seeing this bad thing before?

Before:
<img width="1728" height="1084" alt="before"
src="https://github.com/user-attachments/assets/89659cee-c83c-4c87-8977-d53fec11662e"
/>

After:
<img width="1728" height="1084" alt="after"
src="https://github.com/user-attachments/assets/69747fa8-f3ab-454c-90de-b81d1778c5c2"
/>

Release Notes:

- Fixed excluded language servers starting nonetheless
2026-07-03 08:22:48 +00:00
Miguel Raz Guzmán Macedo
0346a17d09
Add zed: get merch command palette action (#60330)
Adds a `zed: get merch` command palette action that opens
https://merch.zed.dev/ in the system browser.

Just thought it'd be fun!

Release Notes:

- Added a `zed: get merch` action that opens the Zed merch store.
2026-07-03 05:59:13 +00:00
Ben Kunkle
552fc9f3c3
reqwest_client: Fix streamed request bodies being truncated (#60314)
StreamReader::poll_next dropped the reader it take()s whenever the read
returned Poll::Pending, so the next poll reported end-of-stream. And
poll_read_buf measured bytes via ReadBuf::filled(), which never advances
when the reader writes through initialize_unfilled(), so every read
counted as zero bytes. Together these made any AsyncBody::from_reader
request body (e.g. one backed by async_fs::File, which returns Pending
on its first read) upload as an empty or truncated body.

Keep the reader across Pending polls and use the byte count that
futures::AsyncRead::poll_read returns.



- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2026-07-03 01:00:54 +00:00
Marshall Bowers
b5c2d8a13f
Revert "Fix hanging updates after system sleep (#60301)" (#60321)
This reverts commit 2882636c06.

This was causing Zed to crash immediately on startup with the following
error:

```
thread 'main' (74835290) panicked at /Users/maxdeviant/.cargo/git/checkouts/reqwest-dc13ba947e7b959e/c156624/src/async_impl/body.rs:365:33:
there is no reactor running, must be called from the context of a Tokio 1.x runtime
```

Closes FR-118.

Release Notes:

- Reverted https://github.com/zed-industries/zed/pull/60301
2026-07-02 23:54:11 +00:00
Ben Kunkle
bb48a42983
keymap_editor: Fix deleting and editing bindings that use deprecated action names (#60300)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
# Objective

In the keymap editor, bindings whose `keymap.json` entries use a
deprecated action alias (e.g. `"editor::CopyRelativePath"` for
`workspace::CopyRelativePath`) could not be deleted or edited. Aliases
resolve to the canonical action on load, so the row displays normally,
but file updates searched for the canonical name and never matched the
alias entry. Deleting failed ("Failed to find keybinding to remove"),
and editing silently fell back to appending a new binding — leaving the
user with duplicate identical-looking rows they couldn't remove.

## Solution

- Thread gpui's deprecated-alias map
(`App::deprecated_actions_to_preferred_actions`) into
`KeymapFile::update_keybinding`, and resolve aliases in file entries
when matching bindings.
- As a side effect, editing an alias-based binding now rewrites the
entry with the canonical action name instead of duplicating it.

## Testing

- Unit tests in `settings` for removing and replacing alias-based
entries.
- End-to-end keymap editor tests (fake fs → keymap load → `KeymapEditor`
→ delete) covering both the alias case and plain duplicate bindings.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed the keymap editor failing to delete or edit key bindings whose
keymap file entries use deprecated action names
2026-07-02 19:24:34 +00:00
Anthony Eid
2882636c06
Fix hanging updates after system sleep (#60301)
When Zed was downloading an update and the machine went to sleep, the
download would hang indefinitely on wake because the in-flight TCP
connection had silently died and nothing ever timed it out or retried.
This adds an inactivity `read_timeout` to the HTTP client so a stalled
response body errors out instead of hanging forever
(slow-but-progressing downloads are unaffected, since the timeout resets
on each chunk). It also promotes `App::on_system_wake` to a
multi-subscriber `Subscription` API and uses it in the auto-updater to
cancel an interrupted check/download on wake and start a fresh attempt.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fix hanging Zed update downloads after system sleep
2026-07-02 19:13:08 +00:00
Bennet Bo Fenner
4aa8ad9742
agent: Make terminal threads searchable (#60292)
Makes cmd-f work for terminal threads.

Since the agent panel is not part of the normal pane system we have to
manually hook up the search bar.

Closes #57309

Release Notes:

- agent: Added support for searching (cmd-f) inside of terminal threads
2026-07-02 18:38:37 +00:00
Anant Goel
ea87b05794
Fix worktree grouping for bare checkouts (#59968)
Summary

- Track whether a worktree root is itself a linked Git worktree.
- Use that metadata when computing project group keys so bare checkout
worktrees group under the repository identity path.
- Propagate the metadata through remote worktree protocols and add
local/remote regression coverage.

Background

Bare checkout layouts can place linked worktrees under the repository
identity directory, e.g. `/monty/.bare` with worktrees like
`/monty/feature-a`. We were treating those linked worktree paths as
separate project identities, which caused the sidebar to move agent
threads under the active worktree instead of the shared repository
group.
We also exclude adding this to collab intentionally, we can open a
different PR for that if we need to.

Closes #59910
Closes AI-431

Test Plan

- `cargo fmt --package project --package worktree --package
remote_server --package workspace --package collab --package proto`
- `git --no-pager diff --check`
- `cargo test -p project test_project_group_key -- --nocapture`
- `cargo test -p remote_server test_remote_root_repo_common_dir --
--nocapture`
- `cargo test -p worktree remote_worktree -- --nocapture`
- `cargo test -p workspace
test_remote_project_root_dir_changes_update_groups -- --nocapture`
- `cargo check -p collab`

Self-Review Checklist:

 I've reviewed my own diff for quality, security, and reliability
 Unsafe blocks (if any) have justifying comments
The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
 Tests cover the new/changed behavior
 Performance impact has been considered and is acceptable

Release Notes:

- Fixed agent thread/sidebar grouping for Git worktrees backed by bare
checkouts.

---------

Co-authored-by: Anthony Eid <anthony@zed.dev>
2026-07-02 18:14:11 +00:00
justinschmitz97
31fc9d5f47
project_panel: Continue batch delete when individual entries fail (#59595)
## Summary

On Windows, `trash::delete_with_info` can return an error even when the
file was successfully moved to the Recycle Bin.

This is caused by a race condition in the `trash` crate: after
`IFileOperation::PerformOperations` completes, it re-binds the trashed
item via `SHCreateItemFromParsingName` to read its metadata, but the
shell's virtual namespace cache may not yet reflect the new item,
producing an error. Previously, the delete loop propagated this error
and aborted the entire batch. The outer `detach_and_log_err` swallowed
the error, so the user received no feedback.

This PR handles each entry independently: entries that fail to delete
are skipped, the loop continues, and undo history is recorded for the
entries that succeeded.

The upstream root cause (the post-operation re-bind race in
`zed-industries/trash-rs`) will be addressed separately.

Before:


https://github.com/user-attachments/assets/59a95ac5-098b-42dc-bffb-f3240c6b966d

After:


https://github.com/user-attachments/assets/0cd6c0b3-2ee3-4bf2-a243-3e8a83d05dd7



## Test plan

- [ ] Select multiple files in the project panel on Windows
- [ ] Trash them (right-click > Move to Trash, or `Delete` key)
- [ ] Confirm all selected files are deleted, even if one triggers the
trash-crate race
- [ ] Undo restores the files that were successfully trashed

Release Notes:

- Improved handling of failed trash or delete operations in the Project
Panel in order to display a toast informing the user that some files
could not be trashed or deleted

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
2026-07-02 16:04:53 +00:00
Ibrahim Khan
27a9e2057b
vim: Fix Helix cursor position when deleting to end of line (#59987)
## Why / What

Fixes #58702.

In Helix mode the block cursor is a one-character selection that can
rest on the
trailing newline at the end of a line. When you select to the end of a
line
(excluding the newline) and press `d`, Helix leaves the cursor on the
newline.
Zed instead clamped the cursor onto the character immediately to the
left of the
deleted selection.

The shared `visual_delete` always re-clipped the post-deletion cursor at
line
ends (`set_clip_at_line_ends(true)`), which is correct for Vim normal
mode but
contradicts Helix mode, where `Vim::clip_at_line_ends()` is `false`.
This also
made Helix inconsistent with itself: deleting a whole line left the
cursor on the
newline, but deleting to the end of a non-empty line clamped it onto the
previous
character. The fix honors the active mode's clip policy, so Helix keeps
the
cursor on the newline.

## Testing

- Added `test_delete_to_end_of_line_keeps_cursor_on_newline` reproducing
#58702.
- Corrected `test_helix_select_end_of_line`; its mid-line assertion had
captured
the pre-fix (clamped) cursor position. Its whole-line assertion already
expected the cursor on the newline, so the test is now internally
consistent.
- `cargo test -p vim`: 540 passed, 0 failed.
- Vim visual-mode delete is unchanged (`is_helix()` is false for Vim
modes).

Release Notes:

- Fixed Helix mode placing the cursor on the wrong character after
deleting a selection that ends at the end of a line.

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
2026-07-02 15:38:22 +00:00
saberoueslati
e1ec575d3f
editor: Update gutter hover tooltip on modifier changes (#58880)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
## Context

The gutter hover button can represent either adding a breakpoint or
adding a bookmark depending on whether the secondary modifier is
pressed. Previously, the visible tooltip did not update when the
modifier changed while the popover was already open. This moves the
gutter hover intent into a shared state used by both the button and
tooltip, so the label, metadata, icon, color, click action, and
displayed keybinding all stay in sync.

Closes #58821

## How to Review

`crates/editor/src/editor.rs` adds a shared gutter hover intent type and
a custom tooltip view that reads the current modifier state while
rendering. Review that bookmark and breakpoint behavior map consistently
across the button icon/color, click action, tooltip label, metadata, and
keybinding.

`crates/editor/src/editor_tests.rs` adds a regression test covering the
tooltip intent selection with and without the secondary modifier.

Manual test after the fix below :

[Screencast from 2026-06-09
01-30-23.webm](https://github.com/user-attachments/assets/ef465eb9-e659-44da-93a6-a511c84e9401)

## Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed the breakpoint/bookmark gutter popover not updating when
modifier keys change.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2026-07-02 14:38:04 +00:00
Bennet Bo Fenner
4ff55d09e5
Refactor agent settings UI code (#60274)
This PR removes/simplifies some APIs that were leftover after moving the
agent settings from the agent panel into the agent settings UI.
Behavior/UI should be identical.

- Removed `ConfigurationViewTargetAgent` since only a single variant was
used
- Removed `configuration_view` and `configuration_view_v2` and replaced
it with `settings_view`
- Removed all the custom configuration views that were replaced by the
API key view abstraction
- Removed `intitial_title` and `initial_description` and moved it to
`ProviderSettingsView`

Release Notes:

- N/A
2026-07-02 14:32:34 +00:00
ozacod
17090674b3
text_finder: Add collapsible file groups (#60193)
## Summary

Add a chevron (`Disclosure`) to each file group header in Text Finder,
letting you fold/unfold that file's matches. Fold state is cleared
whenever a new search starts.

## Test plan

- [x] Open Text Finder, run a search with matches in multiple files
- [x] Click a header's chevron, confirm its matches hide/show and
selection lands on a visible row
- [x] Start a new search, confirm folded state doesn't carry over

Release Notes:

- Added the ability to collapse per-file match groups in Text Finder

---------

Co-authored-by: ozacod <ozacod@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2026-07-02 13:49:41 +00:00
Xiaobo Liu
78b6bf2fbe
command_palette: Show scrollbar in command palette (#60239)
Enable the command palette picker scrollbar while preserving the
one-shot reopen behavior.

---

Release Notes:

- N/A or Added/Fixed/Improved ...

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2026-07-02 13:23:17 +00:00
Smit Barmase
7eb4cb2bfa
workspace: Fix panic when discarding a draft workspace (#60279)
To reproduce:

1. Open a git project with the agent sidebar, ideally with no other
threads in the sidebar.
2. Create a thread in a new git worktree and type into it without
sending.
3. While still in that worktree workspace, hit New Thread. The parked
draft must now be the last activatable entry in the sidebar.
4. Hover the parked draft → click Discard Draft
5. 💥

Release Notes:

- Fixed a panic when discarding a draft workspace.
2026-07-02 13:13:46 +00:00
Neel
969c6c719c
editor: Fix panic when restoring empty selections on undo/redo (#59372)
Closes FR-81.

Also updates the error log, as the linked issue is closed.

Release Notes:

- Fixed panic when restoring empty selections on undo/redo
2026-07-02 13:07:42 +00:00
Neel
2ec0e6c2d7
editor: Fix panic when confirming completion across buffers (#59471)
In multi-buffer context, `do_completion` resolved the completion's
replace range anchors against whichever buffer currently held the newest
selection. If that selection had moved into a different file's excerpt
(while the menu remained open), the anchors belonged to a different
buffer and caused a panic.

This PR adds a guard to confirm the completion buffer and selection
buffer is the same.

Closes FR-76.

---

Release Notes:

- Fixed a panic that could occur when confirming a completion in a
multibuffer if the cursor had moved into a different file
2026-07-02 13:07:34 +00:00
Marco Groot
442a3476bc
Fix crash when trashing all untracked files (#60235)
# Objective

Fixes a crash when trashing a large number of untracked files from the
Git Panel, caused by unbounded OS thread spawning (thread explosion).

## Solution

`GitPanel::clean_all()` builds one deletion task per untracked file and
then awaits them one by one. Since GPUI tasks start executing as soon as
they are spawned, not when they are awaited, all deletions run
concurrently. Each of them called `RealFs::trash`, which spawned a
dedicated OS thread per call, so a large enough batch exceeded the OS
per-process thread limit and panicked at the `.expect("The os can spawn
threads")` in `RealFs::trash`.

This PR replaces the dedicated thread with `smol::unblock`, which runs
the blocking `trash::delete_with_info` call on a shared thread pool that
is capped and queues additional work instead of spawning more. This is
the same idiom already used elsewhere in the file (e.g. `atomic_write`)
for blocking filesystem work.

## Testing

- Did you test these changes? If so, how?
yes, reproduced using steps in the issue before and after and confirmed
fix works.


https://github.com/user-attachments/assets/d3bcd049-485a-486f-877a-0e5c21b7917c

Reproduction steps here:
https://github.com/zed-industries/zed/issues/60216

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #60216

---

Release Notes:

- Fixed a crash caused by spawning one thread per file when trashing a
large number of untracked files from the Git Panel.

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
2026-07-02 13:01:57 +00:00
Bennet Bo Fenner
52b61cf424
zed: Respect default_open_behavior when opening file from Finder (#59661)
Follow up to #59551 so that it also works when opening a single file,
e.g. from finder

See #59950

Release Notes:

- N/A
2026-07-02 12:56:04 +00:00
Marshall Bowers
c55693876e
cloud_api_types: Remove unused AcceptTermsOfServiceResponse type (#60226)
This PR removes the `AcceptTermsOfServiceResponse` struct, as it is no
longer used.

Release Notes:

- N/A
2026-07-02 12:38:43 +00:00
Bennet Bo Fenner
21d66eb9d5
settings: Remove unused message_editor setting (#60260)
Used this at one point when we still had chat in Collab, but now it's
unused.

Release Notes:

- N/A
2026-07-02 11:45:37 +00:00
Ankan Misra
4a1cb2b1e2
lsp_button: Fix missing server metadata after restart (#55162)
Restarting an LSP from the picker drops the server's metadata (version,
memory, "View Logs") even though the new server is healthy. Reproduces
with `vtsls`, `eslint`, `rust-analyzer`, and others; `vlsls` doesn't.

The button kept id-keyed state in
[`health_statuses`](https://github.com/zed-industries/zed/blob/main/crates/language_tools/src/lsp_button.rs#L159)
and
[`servers_per_buffer_abs_path`](https://github.com/zed-industries/zed/blob/main/crates/language_tools/src/lsp_button.rs#L161),
but never handled
[`LspStoreEvent::LanguageServerRemoved`](https://github.com/zed-industries/zed/blob/main/crates/project/src/lsp_store.rs#L4066)
— there was even a [stale
TODO](https://github.com/zed-industries/zed/blob/main/crates/language_tools/src/lsp_button.rs#L860)
claiming the event wasn't emitted (it's emitted from [five
sites](https://github.com/zed-industries/zed/blob/main/crates/project/src/lsp_store.rs#L11190)
in `lsp_store`). On restart the dead id lingered next to the new one in
both maps, and rendering picked the wrong entry.

Fix: handle `LanguageServerRemoved` and evict the dead id from both
maps. `binary_statuses` is left alone — it's name-keyed and shared
across restart cycles to drive the "Downloading… → Starting…" UX.

The cleanup is extracted to `LanguageServers::remove_server` so it's
directly unit-testable. Added four tests covering health eviction,
per-buffer eviction with empty-entry pruning, `binary_statuses`
preservation, and the full restart sequence.

[PR #50417](https://github.com/zed-industries/zed/pull/50417) attempted
this earlier with a `LanguageServerAdded` handler. @SomeoneToIgnore
flagged that as the wrong hook ([buffer
registration](https://github.com/zed-industries/zed/blob/main/crates/language_tools/src/lsp_button.rs#L919-L948)
is what populates state, not startup) and asked for tests. Handling
`LanguageServerRemoved` is a cleaner fit — it's the natural counterpart
to registration, and it also fixes the related case where stopping a
server (without restart) leaks state.

## Verification

Reproduced on `main` and verified the fix on this branch with `vtsls`
and `eslint` against a small TypeScript project.

| Before restart | After restart on `main` (bug) | After restart on this
branch (fix) |
|---|---|---|
| <img width="588" alt="vtsls row before restart — version, memory, View
Logs all visible"
src="https://github.com/user-attachments/assets/bdd2610b-cf8f-4ea1-bc45-f363b098c146"
/> | <img width="536" alt="vtsls row after restart on main — metadata
gone"
src="https://github.com/user-attachments/assets/66b3eefd-f801-4bfd-8b80-5f0e20fb3bc6"
/> | <img width="554" alt="vtsls row after restart on fix branch —
metadata persists"
src="https://github.com/user-attachments/assets/51d4df48-1223-4b21-856f-72d4c1eb2a6a"
/> |

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #53627

Release Notes:

- Fixed missing language server metadata in the LSP menu after restart.
</content>

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2026-07-02 11:00:16 +00:00
loadingalias
e5513539ec
editor: Accumulate consecutive KillRingCut line kills (#51761)
Closes #22490

## Summary
• Keep kill-ring state as a dedicated global structure (`text`,
`metadata`, `row`, `column`, `buffer_id`) instead of a raw clipboard
wrapper.
• Update `editor::KillRingCut` to append consecutive empty-line kills
only when all are true:
   - one selection only
   - previous selection was empty
   - same row/column
   - same buffer

• Preserve existing behavior for non-empty and multi-selection cuts by
replacing the ring payload instead of appending.
• Preserve yank metadata behavior through the existing clipboard
selection metadata path.
• Add regression coverage in
`editor_tests::test_kill_ring_cut_accumulates_multi_line_kills`.

## Verification
 • `cargo fmt --all -- --check`
 • `./script/check-keymaps`
 • `./script/clippy -p editor`
 • `cargo test -p editor -- --nocapture`
• `cargo test -p editor
editor_tests::test_kill_ring_cut_accumulates_multi_line_kills --
--exact`
 • `cargo test -p editor test_cut_line_ends -- --exact`

## Manual
• `Ctrl+K`, `Ctrl+K`, `Ctrl+Y` with repeated line-end kills at same
caret location pastes concatenated text.
• Non-empty selection cut and multi-selection still replace as before
(no unintended append).
 • Cut in one buffer then cut in another does not cross-append.
 • Moving caret between kills does not append.
 • Undo/redo around cut+yank path works cleanly.

Release Notes:

- Fixed Emacs-style kill-ring behavior so successive `KillRingCut`
operations at the same caret context are accumulated and pasted together
via one `KillRingYank`, while preserving existing non-empty selection
and multi-selection semantics.

---------

Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
2026-07-02 10:16:01 +00:00
./daniele
da2b62c917
Fix relative line number calculation when the first row is wrapped (#53759)
fixes #53726
Compute the base delta from the first visible, non-filtered row instead
of using rows.start. This avoids off-by-one numbering when rows.start is
a wrapped continuation that gets filtered out. Ensure deleted lines
still advance the relative counter even when they are not numbered. Add
a test for the wrapped scrolling case.
P.S. This is my first time using rust for a serious pull request and
let's say I'm a little bit rusty with it.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #ISSUE

Release Notes:

- N/A or Added/Fixed/Improved ...

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-02 09:49:52 +00:00
mertkanakkoc
9375695626
Project/use line hint for search (#58871)
Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

This PR makes two changes:

1. detect() now returns the line number of the first match instead of
just a boolean.
2. This line number (line_hint) is threaded through the pipeline and
passed as a subrange to query.search in full buffer scan, so the full
buffer scan starts from where the first match was already found instead
of the beginning of the file.
 
 ## Scope

  - This optimization only applies to **local search** (not remote).
- It is only effective for **single-line text and single-line regex**
queries. For multiline queries, `detect()` returns `line_hint = 0`, so
the full buffer is scanned from the beginning as before.
- It does not apply to already-open buffers, which always receive
`line_hint = 0`.
  
  ## Test
i tried to search the word "typeably" in the file
"pkgs/development/haskell-modules/hackage-packages.nix" of the repo
[nixpkgs](https://github.com/NixOS/nixpkgs) given in the comment:
[comment](https://github.com/zed-industries/zed/issues/38799#issuecomment-4088491295).
Result seems promising:
 
**with line_hint: 
search: 45.21ms** 

**without line_hint:
search: 508.05ms** 

I measured the time within the function: "handle_find_all_matches"

## Assumption

The core assumption is: if the search query is long enough (after 4 or 5
chars), the probability of finding multiple matches in a single file is
low. In that case, re-scanning the content before line_hint is
unnecessary — detect() function already confirmed there is no match
there.
 
Changes maybe can make an improvement on issues #38799 and #58905

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2026-07-02 09:46:33 +00:00
Lena
b3d5ead59f
Add Guild board automation (#60266)
Automates the Guild contributor program on project board #74 so the
cohort is recognized and kept unblocked without manual babysitting.

- Move a board issue to In Progress when a Guild member self-assigns it,
and post a friendly heads-up when they are assigned an issue that is not
on the board.
- Flag when a Guild member opens a new PR while another of theirs is
still open, to help them land work before spreading thin.
- Check in on quiet assignments and, if an assignee stays silent, free
the issue back to a to-do column so others can pick it up; a "guild
hold" label lets maintainers pause check-ins after they have followed
up.
- Share a weekly digest of what the Guild shipped.
- Label PRs from Guild members and recognize a Guild contributor tier on
the community PR board.

Release Notes:

- N/A
2026-07-02 09:46:22 +00:00
Ben Brandt
9cb3140ea1
wgpu: Use wgpu from crates.io (#60264)
Our patch made it upstream! So we can go back
https://github.com/gfx-rs/wgpu/releases/tag/v29.0.4

Release Notes:

- N/A
2026-07-02 09:36:27 +00:00
Yue Fei
779ca5ef72
docs: Add note to Windows building docs (#60104)
# Objective

Microsoft has decoupled the MSVC version from the Visual Studio version
starting with Visual Studio 2026 (see [this
blog](https://devblogs.microsoft.com/cppblog/new-release-cadence-and-support-lifecycle-for-msvc-build-tools/)),
meaning that the previous traditional naming conventions were obsolete.
For Visual Studio 2026 (including future newer versions of VS) users,
the existing docs will probably confuse them, as they cannot find out
the components named like "MSVC v145 - VS 2026 C++ ..." in present
Visual Studio Installer.

## Solution

Add a note next to the dependencies.

## Testing

Docs are no need to test. (Maybe?)

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A
2026-07-02 09:11:42 +00:00
Sudarsh
f964172a69
project_panel: Wrap filenames in code spans in confirmation dialogs (#53068)
Closes #53060 

Filenames with double underscores (e.g. __init__.py) were rendered as
Markdown bold in confirmation dialogs because the prompt renderer
interprets the message as Markdown. Wrap filenames in backticks so they
render as inline code instead.

Release Notes:
- Fixed filenames with double underscores rendering as bold in project
panel confirmation dialogs
 
 

<img width="327" height="186" alt="image"
src="https://github.com/user-attachments/assets/ea178055-e7e9-4a4c-80de-587fda53d894"
/>
<img width="328" height="129" alt="image"
src="https://github.com/user-attachments/assets/de93eb43-da71-41b1-9bb2-c02b86205051"
/>
<img width="329" height="175" alt="image"
src="https://github.com/user-attachments/assets/eed19f8a-1a99-4485-abf6-57c506e01e00"
/>

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-02 09:11:26 +00:00
Remco Smits
d1e8c0b50f
git_store: Avoid redundant work in worktree git repository update (#60205)
## Objective

While profiling https://github.com/zed-industries/zed/issues/60102 I
noticed `GitStore::update_repositories_from_worktree` does unneeded
work. It fires on every `WorktreeUpdatedGitRepositories` event, which
under filesystem pressure (`rescan: user dropped`) repeats roughly once
per second and can fire more frequently in a large monorepo with many
repositories, since the redundant work scales with the number of
repositories.

## Solution

In `crates/project/src/git_store.rs`:

- Drop the per-iteration `Arc<Path>` clone in the repo lookup; compare
the borrowed reference instead.
- Hoist the `is_trusted` (`can_trust`) computation out of the loop since
it only depends on `worktree_id`.

No behavior change is intended; this is purely reducing redundant work
per event.

## Notes

The *frequency* of these events originates upstream in
`LocalWorktree::changed_repos` (`crates/worktree/src/worktree.rs`),
which re-emits an `UpdatedGitRepository` whenever `git_dir_scan_id`
changes on a full rescan. That path is intentionally left untouched here
since it involves scan semantics; this PR only addresses the redundant
work on the consumer side.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- N/A
2026-07-02 09:08:18 +00:00
Warya Wayne
7d545c0bae
markdown: Make linked images clickable (#59525)
# Objective

Make images nested inside markdown links (e.g., README badges)
clickable, so they behave like normal markdown links.

Previously, linked images rendered correctly but were not interactive in
markdown previews: they did not show a pointer cursor, could not be
clicked to open their target URL, and did not expose link actions
through the context menu.

## Solution

Changed the `MarkdownElement` to detect when an `<img>` tag is inside a
`<a>` tag during rendering. When a linked image is rendered:

- The wrapper `div` gets a `cursor_pointer` style and an `on_click`
handler that opens the link URL (or delegates to the `on_url_click`
callback if set).
- The wrapper also captures right-click events to populate the context
menu with "Copy Link" via `capture_for_context_menu`.
- The `on_url_click` closure type was changed from `Box<dyn Fn>` to
`Rc<dyn Fn>` to allow cloning across multiple handlers.
- When a linked image fails to load, the fallback element's `on_click`
now calls `cx.stop_propagation()` to prevent the event from bubbling to
the parent link wrapper and opening the URL twice.

The feature is gated behind `!self.style.prevent_mouse_interaction` so
it respects existing interaction-prevention settings.

## Testing

- `cargo test -p markdown`: 95 tests passed.
- Manually verified in markdown preview that badge/image links open
correctly and right-click shows "Copy Link".
- Verified existing markdown tests continue to pass.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed images inside markdown links not being clickable.


https://github.com/user-attachments/assets/df2a8d8f-b002-47f9-b139-815ee713700b

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-02 08:00:19 +00:00
Smit Barmase
d132afe9fc
Add new area labels to track mapping - 2 (#60247)
| Label | Description |
|---|---|
| `area:ai/agent thread/sandbox` | Feedback for Zed's Agent sandboxing |
| `area:text finder` | Issue about text finder |
| `area:gpui/graphics` | Related to graphics issues in GPUI |

Release Notes:

- N/A
2026-07-02 07:42:58 +00:00
Danilo Leal
995e56d263
markdown_preview: Use UI font for base font size (#60212)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
This PR makes the `markdown_preview_font_size` setting be based on the
UI font size instead of the buffer UI font size. I typically use a much
smaller code font than UI font, and that made reading the markdown
preview super small. I don't think this will be uncommon because the
dimensions between mono and non-mono typefaces are different...

Release Notes:

- Changed the markdown preview to fall back to the UI font size instead
of the buffer font size when `markdown_preview_font_size` is unset.
2026-07-02 05:04:24 +00:00
Cameron Mcloughlin
10b0795183
sandbox: Fix WSL downloaded binary path (#60210)
WSL downloads a linux zed binary for the sandbox helper. But the flag is
set on the `zed-editor` binary, not the `zed` cli binary. This fixes
that

---

Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-01 22:52:24 +00:00
( Nechiforel David-Samuel ) NsdHSO
d0802abdec
grammars: Fix runnable gutter detection for describe.skipIf / test.skipIf in JS/TS/TSX (#60153)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
I came across issue #60149 and wanted to help. The runnable gutter (play
button) was not showing for Vitest's conditional test wrappers like
it.skipIf(condition) and it.runIf(condition).
<img width="905" height="395" alt="image"
src="https://github.com/user-attachments/assets/7386ae4c-f2e6-4fe8-9e33-674d6821d3fa"
/>
<img width="972" height="787" alt="image"
src="https://github.com/user-attachments/assets/c7c55bb2-ed50-4ce0-ae31-af341ab9eb3a"
/>
<img width="167" height="488" alt="image"
src="https://github.com/user-attachments/assets/75e07913-390e-4674-a5fb-7113ffd1fa85"
/>

Closes #60149 

Release Notes:

- Fixed runnable gutter detection for Vitest conditional test wrappers
(skipIf/runIf) in JS/TS/TSX files

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-01 17:36:57 +00:00
G36maid
b206841b4b
Add range-based whitespace and newline removal to buffer formatting (#53942)
## Summary

When formatting specific ranges (e.g. via Format Selections), trailing
whitespace removal and final newline enforcement currently operate on
the entire file. This PR makes these operations range-aware, so they
only affect lines within the target ranges.

As noted in #16509, users want `remove_trailing_whitespace_on_save` and
`ensure_final_newline_on_save` to suppress jitter by only touching
changed lines. This PR provides the foundational infrastructure for that
— range-based whitespace and newline operations — which a follow-up PR
will wire into the `format_on_save` modifications mode.

## Changes

- Added `remove_trailing_whitespace_in_ranges` and
`ensure_final_newline_in_range` to `Buffer`
- When a `FormattableBuffer` has ranges set, whitespace and newline
operations now use the range-based variants
- `format_ranges_via_lsp` returns `Ok(None)` instead of an error when
the LSP doesn't support range formatting
- Extracted `anchor_ranges_to_row_ranges` helper to deduplicate
conversion logic

<details><summary>Tests (4)</summary>

- `test_trailing_whitespace_in_ranges` — only modified lines have
whitespace removed
- `test_trailing_whitespace_empty_ranges` — empty ranges → no changes
- `test_final_newline_modified_last_line` — newline added when last line
is in range
- `test_final_newline_unmodified_last_line` — newline not added when
last line is outside range

</details>

---

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Improved Format Selection to scope trailing whitespace removal and
final newline enforcement to the selected range instead of the entire
file
- Improved handling of LSP servers that do not support range formatting
by gracefully skipping instead of producing an error

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-01 17:36:36 +00:00
Kai Kozlov
bcd54d0adf
editor: Add intelligent bracket colorization (#51580)
Closes #50210

Bracket colorization currently runs theme accents through
`ensure_minimum_contrast`, which can significantly alter authored
palette colors in order to satisfy a single foreground-vs-background
target. In practice, that makes rainbow brackets drift away from the
theme colors they are supposed to reflect.

This PR keeps bracket colorization as a single automatic behavior, but
changes how the palette is derived:

- preserve authored accent colors when they already work
- adjust only the specific accents that are too weak against the editor
background
- only reorder the palette when adjacent bracket levels are still too
similar after that

The implementation is staged because bracket colorization has to handle
two separate problems:

- **Bracket-vs-Background Readability**: is an individual bracket
visible against the editor background?
- **Adjacent Bracket Separation**: can you still distinguish one nesting
level from the next?

Those two constraints are also why the implementation cannot simply
defer to whatever accent colors the theme provides. Some themes,
including the bundled One and Ayu, do not define explicit accents at
all, so bracket colorization falls back to Zed's shared default accent
palette. That palette was not designed with any particular theme's
background in mind, which makes the background contrast check especially
important for those cases.

That is why the previous `ensure_minimum_contrast(..., 55.0)` path is
not enough on its own. It addresses foreground-vs-background
readability, but if lightness-only adjustment cannot satisfy that target
it escalates to saturation reduction and then black/white fallback.

This PR instead keeps the bracket-specific background fix bounded and
lightness-only in OKLCH, so the resulting color stays much closer to the
authored accent.

### How it works

- **Background Contrast Correction**: accents that fall below the APCA
floor against the editor background are adjusted by changing OKLCH
lightness only, preserving hue and chroma
- **Adjacency Correction**: if the resulting palette is still too weak
between neighboring nesting levels, the palette is reordered
conservatively to improve adjacent separation

That ordering matters. Doing the reorder first can easily be undone by
the background-fix step.

### Color measurement

- background contrast uses APCA, via the existing `apca_contrast` path
- adjacent bracket separation uses OKLab Euclidean distance

APCA is the right tool for bracket-vs-background readability, but it is
not designed to compare two foreground colors against each other. OKLab
distance is a better fit for detecting when neighboring bracket levels
are perceptually too close.

### Thresholds

- background APCA floor: `30` on dark themes, `35` on light themes
- adjacent OKLab target: `0.08` on dark themes, `0.10` on light themes
- light-theme reorder tolerance band: reorder only triggers below
`0.095`, avoiding threshold-artifact reordering on palettes that are
visually fine but narrowly miss `0.10`

### Validation

- `cargo test -p editor bracket_colorization`
- manual comparison on bundled One, Ayu, and Gruvbox theme families

Release Notes:

- Improved bracket colorization by preserving theme accent colors when
possible and applying targeted contrast fixes only when needed.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2026-07-01 17:36:23 +00:00
Danilo Leal
550ddc9405
agent_ui: Replace thread controls with slash commands (#59974)
Closes AI-432

The turn-end action buttons in the agent panel (scroll to top, scroll to
most recent prompt, open as markdown, thumbs up/down, share/sync) are
now exposed as local slash commands in the message editor instead of
buttons rendered at the end of each turn. The actions themselves are
unchanged — this is just a different way to access them.

Release Notes:

- Changed some of the agent panel's turn-end action buttons into slash
commands in the message editor.

---------

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
2026-07-01 16:55:07 +00:00
mertkanakkoc
7e0f63412c
terminal_view: Use backslash escaping for dropped file paths for mac (#57747)
Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable

When files are dragged into the terminal, paths were formatted using
Rust's Debug format (`{path:?}`), which wraps them in double quotes.
This caused issues with programs like Claude Code that parse bare paths
from terminal input — quoted paths were treated as plain text instead of
file references.

This change replaces double-quote wrapping with backslash escaping for
special shell characters on Unix, which is both valid shell syntax and
compatible with path-parsing tools running in the terminal. On non-Unix
platforms the previous behavior is preserved.

Tested on macOS only. Unable to verify Windows behavior.

Fixes #57471

Release Notes:

- Fixed/ file drag-and-drop into terminal inserting double-quoted paths,
which prevented tools like Claude Code from recognizing them as file
references.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2026-07-01 16:00:54 +00:00
zed-zippy[bot]
35c3d27282
Bump Zed to v1.11.0 (#60209)
Release Notes:

- N/A

Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
2026-07-01 15:24:09 +00:00
Xin Zhao
b083358680
Suppress pet log spam when opening Python projects (#60204)
# Objective

Zed uses `pet` to auto-discover Python virtual environments. When
opening Python projects, there is a large amount of log spam from `pet`
output, which is not useful:

```
2026-07-01T22:32:07+08:00 INFO  [pet::find] find_and_report_envs; search_scope=None
2026-07-01T22:32:07+08:00 INFO  [lsp] starting language server process. binary path: "/usr/bin/ty", working directory: "/home/xin/works/test/test_python", args: ["server"]
2026-07-01T22:32:07+08:00 INFO  [pet::find] locators_phase;
2026-07-01T22:32:07+08:00 INFO  [pet::find] path_search_phase;
2026-07-01T22:32:07+08:00 INFO  [pet::find] locator_find; locator=PipEnv
2026-07-01T22:32:07+08:00 INFO  [pet::find] workspace_search_phase;
2026-07-01T22:32:07+08:00 INFO  [pet::find] locator_find; locator=PyEnv
2026-07-01T22:32:07+08:00 INFO  [pet::find] locator_find; locator=Pixi
2026-07-01T22:32:07+08:00 INFO  [pet::find] locator_find; locator=Conda
2026-07-01T22:32:07+08:00 INFO  [pet::find] locator_find; locator=Uv
2026-07-01T22:32:07+08:00 INFO  [pet::find] locator_find; locator=Poetry
2026-07-01T22:32:07+08:00 INFO  [pet::find] global_virtualenvs_phase;
2026-07-01T22:32:07+08:00 INFO  [pet::find] locator_find; locator=Venv
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=[] executable_count=0
2026-07-01T22:32:07+08:00 INFO  [pet::find] locator_find; locator=VirtualEnv
2026-07-01T22:32:07+08:00 INFO  [pet::find] find_python_environments_in_workspace_folder_recursive; workspace_folder="/home/xin/works/test/test_python" workspace=/home/xin/works/test/test_python
2026-07-01T22:32:07+08:00 INFO  [pet::find] locator_find; locator=Homebrew
2026-07-01T22:32:07+08:00 INFO  [lsp] starting language server process. binary path: "/usr/bin/ruff", working directory: "/home/xin/works/test/test_python", args: ["server"]
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=[] executable_count=0
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=[] executable_count=0
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=[] executable_count=0
2026-07-01T22:32:07+08:00 INFO  [pet::find] locator_find; locator=LinuxGlobal
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=[] executable_count=0
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=[] executable_count=0
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=[] executable_count=0
2026-07-01T22:32:07+08:00 INFO  [pet::find] locator_find; locator=VirtualEnvWrapper
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=[] executable_count=0
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=["/home/xin/works/test/test_python/.venv/bin/python"] executable_count=1
2026-07-01T22:32:07+08:00 INFO  [pet::locators] identify_python_environment_using_locators; env=PythonEnv { executable: "/home/xin/works/test/test_python/.venv/bin/python", prefix: Some("/home/xin/works/test/test_python/.venv"), version: None, symlinks: None } executable=/home/xin/works/test/test_python/.venv/bin/python
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=PyEnv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Pixi
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Conda
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Uv
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=[] executable_count=0
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=[] executable_count=0
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=[] executable_count=0
2026-07-01T22:32:07+08:00 INFO  [pet::find] identify_python_executables_using_locators; executables=["/usr/bin/python", "/usr/bin/python3", "/usr/bin/python3.14"] executable_count=3
2026-07-01T22:32:07+08:00 INFO  [pet::locators] identify_python_environment_using_locators; env=PythonEnv { executable: "/usr/bin/python", prefix: None, version: None, symlinks: None } executable=/usr/bin/python
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=PyEnv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Pixi
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Conda
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Uv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Poetry
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=PipEnv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=VirtualEnvWrapper
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Venv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=VirtualEnv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Homebrew
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=LinuxGlobal
2026-07-01T22:32:07+08:00 INFO  [pet::locators] resolve_python_env; executable=/usr/bin/python
2026-07-01T22:32:07+08:00 INFO  [pet::locators] identify_python_environment_using_locators; env=PythonEnv { executable: "/usr/bin/python3", prefix: None, version: None, symlinks: None } executable=/usr/bin/python3
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=PyEnv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Pixi
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Conda
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Uv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Poetry
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=PipEnv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=VirtualEnvWrapper
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Venv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=VirtualEnv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Homebrew
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=LinuxGlobal
2026-07-01T22:32:07+08:00 INFO  [pet::locators] resolve_python_env; executable=/usr/bin/python3
2026-07-01T22:32:07+08:00 INFO  [pet::locators] identify_python_environment_using_locators; env=PythonEnv { executable: "/usr/bin/python3.14", prefix: None, version: None, symlinks: None } executable=/usr/bin/python3.14
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=PyEnv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Pixi
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Conda
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Uv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Poetry
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=PipEnv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=VirtualEnvWrapper
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Venv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=VirtualEnv
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=Homebrew
2026-07-01T22:32:07+08:00 INFO  [pet::locators] try_from_locator; locator=LinuxGlobal
2026-07-01T22:32:07+08:00 INFO  [pet::locators] resolve_python_env; executable=/usr/bin/python3.14
```


This PR is intended to remove this log spam.

## Solution

Add `pet` to the default log filters. Now, `INFO` logs from `pet` are
suppressed by default.

## Testing

Tested locally, and verified that there is no longer any `pet` log spam
in the Zed logs.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable


---

Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-01 15:18:54 +00:00
536 changed files with 41812 additions and 11109 deletions

View file

@ -0,0 +1,17 @@
---
name: lint-creator
description: An auxiliary skill to add more dylints to `tooling/lints`
disable-model-invocation: false
---
# Lint RULES
1. Every lint MUST have accompanying `ui` tests
2. `ui` tests MUST be in the `ui` folder
3. Every lint MUST be in a separate module
4. Every lint MUST have negative `ui` tests
5. Lints should be as simple as possible.
6. Reporting is fine if it's simple, it does not need to be elaborate or lengthy code.
7. Do NOT suggest how to fix the lint, only flag it.
8. Do NOT make lints machine applicable.
9. Detect if lints are redundant vs clippy's capabilities.

View file

@ -1,6 +1,11 @@
export default {
async fetch(request, _env, _ctx) {
const url = new URL(request.url);
const acceptHeader = request.headers.get("Accept") || "";
const wantsMarkdown = acceptHeader
.split(",")
.map((mediaType) => mediaType.split(";")[0].trim().toLowerCase())
.includes("text/markdown");
if (url.pathname === "/docs/nightly") {
url.hostname = "docs-nightly.pages.dev";
@ -18,6 +23,14 @@ export default {
url.hostname = "docs-anw.pages.dev";
}
if (url.pathname === "/docs.md") {
url.pathname = "/docs/getting-started.md";
}
if (wantsMarkdown) {
url.pathname = markdownPathFor(url.pathname);
}
let res = await fetch(url, request);
if (res.status === 404) {
@ -27,3 +40,31 @@ export default {
return res;
},
};
function markdownPathFor(pathname) {
if (pathname === "/docs" || pathname === "/docs/") {
return "/docs/getting-started.md";
}
if (pathname.endsWith("/index.md")) {
return pathname.replace(/\/index\.md$/, "/getting-started.md");
}
if (pathname.endsWith(".md")) {
return pathname;
}
if (pathname.endsWith(".html")) {
return pathname.replace(/\.html$/, ".md");
}
if (pathname.split("/").pop().includes(".")) {
return pathname;
}
if (pathname.endsWith("/")) {
return `${pathname}getting-started.md`;
}
return `${pathname}.md`;
}

View file

@ -304,7 +304,6 @@
/crates/picker/ @zed-industries/ui-team
/crates/refineable/ @zed-industries/ui-team
/crates/story/ @zed-industries/ui-team
/crates/storybook/ @zed-industries/ui-team
/crates/svg_preview/ @zed-industries/ui-team
/crates/tab_switcher/ @zed-industries/ui-team
/crates/theme/ @zed-industries/ui-team

View file

@ -22,6 +22,8 @@ on:
description: body
type: string
default: ''
permissions:
contents: read
jobs:
rebuild_releases_page:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -13,9 +13,14 @@ on:
description: run_clippy
type: boolean
default: 'true'
permissions:
contents: read
jobs:
run_autofix:
runs-on: namespace-profile-16x32-ubuntu-2204
permissions:
contents: read
pull-requests: read
env:
CC: clang
CXX: clang++

View file

@ -5,10 +5,15 @@ on:
# Fire every day at 16:00 UTC (At the start of the US workday)
- cron: "0 16 * * *"
permissions:
contents: read
jobs:
update-collab-staging-tag:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1

View file

@ -8,10 +8,14 @@ on:
description: Branch name to run on
required: true
type: string
permissions:
contents: read
jobs:
run_bump_patch_version:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-16x32-ubuntu-2204
permissions:
contents: write
steps:
- id: generate-token
name: steps::authenticate_as_zippy

View file

@ -8,6 +8,8 @@ on:
description: 'Which channels to bump: all, main, preview, or stable'
type: string
default: all
permissions:
contents: read
jobs:
resolve_versions:
if: github.repository_owner == 'zed-industries'
@ -86,6 +88,9 @@ jobs:
- resolve_versions
if: inputs.target == 'all' || inputs.target == 'main'
runs-on: namespace-profile-16x32-ubuntu-2204
permissions:
contents: write
pull-requests: write
steps:
- id: generate-token
name: steps::authenticate_as_zippy
@ -127,6 +132,8 @@ jobs:
- resolve_versions
if: inputs.target == 'all' || inputs.target == 'preview'
runs-on: namespace-profile-16x32-ubuntu-2204
permissions:
contents: write
steps:
- id: generate-token
name: steps::authenticate_as_zippy
@ -180,6 +187,8 @@ jobs:
- resolve_versions
if: inputs.target == 'all' || inputs.target == 'stable'
runs-on: namespace-profile-16x32-ubuntu-2204
permissions:
contents: write
steps:
- id: generate-token
name: steps::authenticate_as_zippy

View file

@ -21,14 +21,12 @@ on:
description: pr_number
required: true
type: string
permissions:
contents: read
jobs:
run_cherry_pick:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- id: generate-token
name: steps::authenticate_as_zippy
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
@ -38,6 +36,11 @@ jobs:
permission-contents: write
permission-workflows: write
permission-pull-requests: write
- name: steps::checkout_repo
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
token: ${{ steps.generate-token.outputs.token }}
- name: cherry_pick::run_cherry_pick::cherry_pick
run: ./script/cherry-pick "$BRANCH" "$COMMIT" "$CHANNEL"
env:

View file

@ -14,6 +14,8 @@ concurrency:
group: potential-duplicate-check-${{ github.event.issue.number || inputs.issue_number }}
cancel-in-progress: true
permissions: {}
jobs:
identify-duplicates:
# For manual testing, allow running on any branch; for automatic runs, only on main repo

View file

@ -13,10 +13,15 @@ on:
type: number
default: 1000
permissions:
contents: read
jobs:
stale:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
permissions:
issues: write
steps:
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
with:

View file

@ -5,10 +5,16 @@ on:
- cron: "0 */12 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
update_top_ranking_issues:
runs-on: namespace-profile-2x4-ubuntu-2404
if: github.repository == 'zed-industries/zed'
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Set up uv

View file

@ -5,10 +5,16 @@ on:
- cron: "0 15 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
update_top_ranking_issues:
runs-on: namespace-profile-2x4-ubuntu-2404
if: github.repository == 'zed-industries/zed'
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Set up uv

View file

@ -7,6 +7,8 @@ on:
schedule:
- cron: 30 17 * * 2
workflow_dispatch: {}
permissions:
contents: read
jobs:
scheduled_compliance_check:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -4,6 +4,9 @@ on:
push:
branches: [main]
permissions:
contents: read
jobs:
check-author:
if: ${{ github.repository_owner == 'zed-industries' }}

View file

@ -11,6 +11,8 @@ on:
- edited
branches:
- main
permissions:
contents: read
jobs:
danger:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -7,6 +7,8 @@ on:
push:
tags:
- collab-production
permissions:
contents: read
jobs:
style:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -35,6 +35,8 @@ on:
description: Git ref to checkout and deploy. Defaults to event SHA when omitted.
type: string
default: ''
permissions:
contents: read
jobs:
deploy_docs:
if: github.repository_owner == 'zed-industries'

View file

@ -5,6 +5,7 @@ on:
push:
branches:
- main
permissions: {}
jobs:
deploy_docs:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -42,6 +42,10 @@ on:
- immediate
default: batch
# Both jobs below declare their own `permissions:` blocks, so no job uses this
# top-level default. Least privilege therefore grants nothing here.
permissions: {}
env:
DROID_MODEL: claude-sonnet-4-5-20250929
SUGGESTIONS_BRANCH: docs/suggestions-pending

View file

@ -10,6 +10,8 @@ on:
- '!extensions/test-extension/**'
- '!extensions/workflows/**'
- '!extensions/*.md'
permissions:
contents: read
jobs:
detect_changed_extensions:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -28,6 +28,8 @@ on:
app-secret:
description: The app secret for the corresponding app ID
required: true
permissions:
contents: read
jobs:
check_version_changed:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -15,6 +15,8 @@ on:
description: working-directory
type: string
default: .
permissions:
contents: read
jobs:
orchestrate:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -14,9 +14,11 @@ on:
description: Description for the changes to be expected with this rollout
type: string
default: ''
permissions:
contents: read
jobs:
fetch_extension_repos:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.ref == 'refs/heads/main'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && (github.ref == 'refs/tags/extension-workflows' || github.ref == 'refs/heads/main')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: checkout_zed_repo
@ -220,21 +222,18 @@ jobs:
clean: false
fetch-depth: 0
token: ${{ steps.generate-token.outputs.token }}
- name: extension_workflow_rollout::create_rollout_tag::update_rollout_tag
run: |
if git rev-parse "extension-workflows" >/dev/null 2>&1; then
git tag -d "extension-workflows"
git push origin ":refs/tags/extension-workflows" || true
fi
echo "Creating new tag 'extension-workflows' at $(git rev-parse --short HEAD)"
git tag "extension-workflows"
git push origin "extension-workflows"
env:
GIT_AUTHOR_NAME: zed-zippy[bot]
GIT_AUTHOR_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
GIT_COMMITTER_NAME: zed-zippy[bot]
GIT_COMMITTER_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
- name: steps::update_tag
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b
with:
script: |
github.rest.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'tags/extension-workflows',
sha: context.sha,
force: true
})
github-token: ${{ steps.generate-token.outputs.token }}
timeout-minutes: 1
defaults:
run:

View file

@ -4,6 +4,9 @@ on:
issues:
types: [labeled]
permissions:
contents: read
jobs:
handle-good-first-issue:
if: github.event.label.name == '.contrib/good first issue' && github.repository_owner == 'zed-industries'

View file

@ -0,0 +1,59 @@
# Guild board (https://github.com/orgs/zed-industries/projects/74) reactions to issue events:
# assigned guild member -> Status "In Progress" (or Slack if off-board)
# unassigned guild member -> move back to a To-Do column by Type + Slack
# commented guild assignee comments after a check-in -> Slack (each comment)
name: Guild Assignment Status
on:
issues:
types: [assigned, unassigned]
issue_comment:
types: [created]
permissions:
contents: read
concurrency:
group: guild-assignment-status-${{ github.event.issue.number || github.run_id }}
cancel-in-progress: false
jobs:
handle-event:
if: >-
github.repository == 'zed-industries/zed' &&
(github.event_name != 'issue_comment' || github.event.issue.pull_request == null)
runs-on: namespace-profile-2x4-ubuntu-2404
timeout-minutes: 5
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
owner: zed-industries
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
sparse-checkout: |
script/github-guild-board.py
sparse-checkout-cone-mode: false
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Install dependencies
run: pip install requests
- name: Handle issue event
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
PROJECT_NUMBER: "74"
GUILD_MODE: event
SLACK_WEBHOOK_GUILD_INTERNAL: ${{ secrets.SLACK_WEBHOOK_GUILD_INTERNAL }}
run: python script/github-guild-board.py

View file

@ -0,0 +1,120 @@
# When a guild member opens a PR while already having another open PR in
# zed-industries/zed, post a heads-up to #zed-guild-internal. PRs opened before
# CUTOFF_DATE (the cohort start) don't count as "already open".
name: Guild New PR Notification
on:
pull_request_target:
types: [opened]
permissions:
contents: read
concurrency:
group: guild-new-pr-notify-${{ github.event.pull_request.number }}
cancel-in-progress: false
env:
CUTOFF_DATE: "2026-07-01"
jobs:
notify-slack:
if: github.repository == 'zed-industries/zed'
runs-on: namespace-profile-2x4-ubuntu-2404
timeout-minutes: 5
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
owner: zed-industries
- name: Notify on an additional open PR by a guild member
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env:
SLACK_WEBHOOK_GUILD_INTERNAL: ${{ secrets.SLACK_WEBHOOK_GUILD_INTERNAL }}
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
// Guild cohort members are outside collaborators holding this custom
// repository role, not members of an org team.
const GUILD_ROLE_NAME = 'Guild Assign issues/PRs';
const pr = context.payload.pull_request;
const author = pr.user.login;
const isGuildMember = async (username) => {
try {
const response = await github.rest.repos.getCollaboratorPermissionLevel({
owner: 'zed-industries',
repo: 'zed',
username
});
// role_name is the effective (highest) role; for cohort outside
// collaborators that is the custom role. Built-in roles come back
// lowercased and won't match.
return (response.data.role_name || '').toLowerCase() === GUILD_ROLE_NAME.toLowerCase();
} catch (error) {
if (error.status === 404) {
return false;
}
throw error;
}
};
if (!(await isGuildMember(author))) {
console.log(`${author} is not a guild member, skipping`);
return;
}
const cutoff = process.env.CUTOFF_DATE;
const query = `repo:zed-industries/zed is:pr is:open author:${author} created:>=${cutoff}`;
const result = await github.rest.search.issuesAndPullRequests({ q: query, per_page: 100 });
const others = result.data.items.filter((item) => item.number !== pr.number);
if (others.length === 0) {
console.log(`${author} has no other qualifying open PRs, skipping`);
return;
}
const webhook = process.env.SLACK_WEBHOOK_GUILD_INTERNAL;
if (!webhook) {
core.setFailed('SLACK_WEBHOOK_GUILD_INTERNAL secret is not set');
return;
}
// Escape Slack's control characters so PR titles render literally
// instead of being read as links or @-mentions.
const esc = (text) =>
(text || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const quips = [
'Deep into the queue they wander.',
'Nevermore just one open PR.',
];
const quip = quips[Math.floor(Math.random() * quips.length)];
const otherList = others
.map((item) => `<${item.html_url}|#${item.number}> ${esc(item.title)}`)
.join('\n• ');
const message =
`${quip} @${author} just opened <${pr.html_url}|${esc(pr.title)}> ` +
`while still having open PR(s):\n• ${otherList}\n` +
'A gentle nudge to help them land one before spreading thin might be in order.';
const response = await fetch(webhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `${author} opened another PR while others are still open`,
blocks: [{ type: 'section', text: { type: 'mrkdwn', text: message } }],
}),
});
if (!response.ok) {
const body = await response.text();
core.setFailed(`Slack notification failed with status ${response.status}: ${body}`);
return;
}
console.log(`Notified: ${author} has ${others.length} other open PR(s)`);

View file

@ -0,0 +1,57 @@
# Scheduled sweep of the Guild board (https://github.com/orgs/zed-industries/projects/74).
# For an "In Progress" issue assigned to a guild member with no linked PR: post a
# check-in comment once the assignee goes quiet, re-nudging after any renewed
# silence, and clear the assignment (moving the issue back to a To-Do column by
# Type) if a check-in goes unanswered. The "guild hold" label pauses the sweep.
name: Guild Stale Assignments
on:
schedule:
- cron: "0 8 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: guild-stale-assignments
cancel-in-progress: true
jobs:
sweep:
if: github.repository == 'zed-industries/zed'
runs-on: namespace-profile-2x4-ubuntu-2404
timeout-minutes: 15
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
owner: zed-industries
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
sparse-checkout: |
script/github-guild-board.py
sparse-checkout-cone-mode: false
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Install dependencies
run: pip install requests
- name: Sweep stale assignments
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
PROJECT_NUMBER: "74"
GUILD_MODE: stale
SLACK_WEBHOOK_GUILD_INTERNAL: ${{ secrets.SLACK_WEBHOOK_GUILD_INTERNAL }}
run: python script/github-guild-board.py

View file

@ -0,0 +1,55 @@
# Scheduled Slack digest of Guild board
# (https://github.com/orgs/zed-industries/projects/74) issues recently closed by
# a merged PR authored by a guild member.
name: Guild Weekly Shipped
on:
schedule:
- cron: "0 7 * * 3"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: guild-weekly-shipped
cancel-in-progress: true
jobs:
digest:
if: github.repository == 'zed-industries/zed'
runs-on: namespace-profile-2x4-ubuntu-2404
timeout-minutes: 15
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
owner: zed-industries
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
sparse-checkout: |
script/github-guild-board.py
sparse-checkout-cone-mode: false
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Install dependencies
run: pip install requests
- name: Build and send digest
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
PROJECT_NUMBER: "74"
GUILD_MODE: weekly
SLACK_WEBHOOK_GUILD_INTERNAL: ${{ secrets.SLACK_WEBHOOK_GUILD_INTERNAL }}
run: python script/github-guild-board.py

View file

@ -21,12 +21,13 @@ on:
permissions:
contents: read
pull-requests: read
jobs:
check-hotfix-reviews:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
permissions:
pull-requests: read
timeout-minutes: 5
env:
REPO: ${{ github.repository }}

View file

@ -9,6 +9,8 @@ on:
types:
- labeled
- synchronize
permissions:
contents: read
jobs:
build_nix_linux_x86_64:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && ((github.event.action == 'labeled' && (github.event.label.name == 'run-nix' || github.event.label.name == 'run-bundling')) || (github.event.action == 'synchronize' && (contains(github.event.pull_request.labels.*.name, 'run-nix') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))))

View file

@ -41,47 +41,9 @@ jobs:
const STAFF_TEAM_SLUG = 'staff';
const FIRST_CONTRIBUTION_LABEL = 'first contribution';
const GUILD_LABEL = 'guild';
const GUILD_MEMBERS = [
'11happy',
'AidanV',
'alanpjohn',
'AmaanBilwar',
'arjunkomath',
'austincummings',
'ayushk-1801',
'criticic',
'dongdong867',
'emamulandalib',
'eureka928',
'feitreim',
'iam-liam',
'iksuddle',
'ishaksebsib',
'lingyaochu',
'loadingalias',
'marcocondrache',
'mchisolm0',
'MostlyKIGuess',
'nairadithya',
'nihalxkumar',
'notJoon',
'OmChillure',
'Palanikannan1437',
'polyesterswing',
'prayanshchh',
'razeghi71',
'sarmadgulzar',
'seanstrom',
'Shivansh-25',
'SkandaBhat',
'th0jensen',
'tommyming',
'transitoryangel',
'TwistingTwists',
'virajbhartiya',
'YEDASAVG',
'Ziqi-Yang',
];
// Guild cohort members are outside collaborators holding this custom
// repository role, not members of an org team.
const GUILD_ROLE_NAME = 'Guild Assign issues/PRs';
const COMMUNITY_CHAMPION_LABEL = 'community champion';
const COMMUNITY_CHAMPIONS = [
'0x2CA',
@ -161,11 +123,11 @@ jobs:
return members.some((member) => member.toLowerCase() === authorLower);
};
const isStaffMember = async (author) => {
const isTeamMember = async (teamSlug, author) => {
try {
const response = await github.rest.teams.getMembershipForUserInOrg({
org: 'zed-industries',
team_slug: STAFF_TEAM_SLUG,
team_slug: teamSlug,
username: author
});
return response.data.state === 'active';
@ -177,6 +139,27 @@ jobs:
}
};
const isStaffMember = (author) => isTeamMember(STAFF_TEAM_SLUG, author);
const isGuildMember = async (author) => {
try {
const response = await github.rest.repos.getCollaboratorPermissionLevel({
owner: 'zed-industries',
repo: 'zed',
username: author
});
// role_name is the effective (highest) role; for cohort outside
// collaborators that is the custom role. Built-in roles come back
// lowercased and won't match.
return (response.data.role_name || '').toLowerCase() === GUILD_ROLE_NAME.toLowerCase();
} catch (error) {
if (error.status !== 404) {
throw error;
}
return false;
}
};
const getIssueLabels = () => {
if (listIncludesAuthor(COMMUNITY_CHAMPIONS, author)) {
return [COMMUNITY_CHAMPION_LABEL];
@ -202,7 +185,7 @@ jobs:
labelsToAdd.push(COMMUNITY_CHAMPION_LABEL);
}
if (listIncludesAuthor(GUILD_MEMBERS, author)) {
if (await isGuildMember(author)) {
labelsToAdd.push(GUILD_LABEL);
}

View file

@ -11,6 +11,8 @@ on:
description: Describe why the extension CLI is being bumped and/or what changes are included.
required: true
type: string
permissions:
contents: read
jobs:
publish_job:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.ref == 'refs/heads/main'

View file

@ -8,6 +8,8 @@ on:
push:
tags:
- v*
permissions:
contents: read
jobs:
run_tests_mac:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@ -723,6 +725,8 @@ jobs:
- bundle_windows_aarch64
- bundle_windows_x86_64
runs-on: namespace-profile-4x8-ubuntu-2204
permissions:
contents: write
steps:
- name: release::download_workflow_artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
@ -756,6 +760,8 @@ jobs:
needs:
- upload_release_assets
runs-on: namespace-profile-2x4-ubuntu-2404
permissions:
contents: write
steps:
- name: release::validate_release_assets
run: |

View file

@ -8,6 +8,8 @@ on:
schedule:
- cron: 0 */4 * * *
workflow_dispatch: {}
permissions:
contents: read
jobs:
check_nightly_tag:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')

View file

@ -9,6 +9,8 @@ on:
types:
- labeled
- synchronize
permissions:
contents: read
jobs:
bundle_linux_aarch64:
if: |-

View file

@ -14,6 +14,8 @@ on:
branches:
- main
- v[0-9]+.[0-9]+.x
permissions:
contents: read
jobs:
orchestrate:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@ -73,12 +75,17 @@ jobs:
# Map directory names to package names
FILE_CHANGED_PKGS=""
for dir in $CHANGED_DIRS; do
pkg=$(echo "$DIR_TO_PKG" | grep "^${dir}=" | cut -d= -f2 | head -1)
pkg=$(echo "$DIR_TO_PKG" | grep "^${dir}=" | cut -d= -f2 | head -1 || true)
# Only add directories that map to a real root-workspace package.
# Some directories (e.g. tooling/lints) belong to a separate workspace
# and are not root members, so they have no mapping here. Previously we
# fell back to the raw directory name, which fabricated a bogus package
# (e.g. "lints") and produced a nextest filter like rdeps(lints) that
# hard-errors ("operator didn't match any packages"). Skipping such
# directories leaves the package set empty, which falls through to the
# "run all tests" path below.
if [ -n "$pkg" ]; then
FILE_CHANGED_PKGS=$(printf '%s\n%s' "$FILE_CHANGED_PKGS" "$pkg")
else
# Fall back to directory name if no mapping found
FILE_CHANGED_PKGS=$(printf '%s\n%s' "$FILE_CHANGED_PKGS" "$dir")
fi
done
FILE_CHANGED_PKGS=$(echo "$FILE_CHANGED_PKGS" | grep -v '^$' | sort -u || true)

View file

@ -12,8 +12,15 @@ on:
- "Community PR Board"
- "PR Board Meta Fields Refresh"
- "PR Issue Labeler"
- "Guild Assignment Status"
- "Guild Stale Assignments"
- "Guild Weekly Shipped"
- "Guild New PR Notification"
types: [completed]
permissions:
contents: read
jobs:
notify-slack:
if: >-

View file

@ -4,32 +4,48 @@ on:
issues:
types: [labeled]
permissions:
contents: read
env:
PRIORITY_LABELS: '["priority:P0", "priority:P1"]'
REPRODUCIBLE_LABEL: 'state:reproducible'
FREQUENCY_LABELS: '["frequency:always", "frequency:common"]'
MARKER_REACTION: 'eyes'
jobs:
notify-slack:
if: github.repository_owner == 'zed-industries' && github.event.issue.state == 'open'
runs-on: namespace-profile-2x4-ubuntu-2404
# Serialize per-issue so concurrent `labeled` events can't both observe
# the trifecta and double-notify.
permissions:
contents: read
# For the issue reaction used to dedupe notifications.
issues: write
# Serialize per issue so the reaction claim below can't race.
concurrency:
group: slack-notify-first-responders-${{ github.event.issue.number }}
cancel-in-progress: false
steps:
- name: Check if label combination requires first responder notification
- name: Check label combination and claim the notification
id: check-label
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
LABEL_NAME: ${{ github.event.label.name }}
ISSUE_LABELS_JSON: ${{ toJson(github.event.issue.labels.*.name) }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
set -euo pipefail
# Gate on the just-added label so unrelated labeling on an
# already-qualifying issue doesn't re-fire the notification.
api() {
curl -sS \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$@"
}
# Ignore labels outside the trifecta so unrelated later edits don't re-fire.
TRIGGER_LABELS=$(jq -cn \
--argjson priority "$PRIORITY_LABELS" \
--arg repro "$REPRODUCIBLE_LABEL" \
@ -42,19 +58,52 @@ jobs:
exit 0
fi
# The webhook's issue.labels snapshot is racy under bulk apply; read live.
ISSUE_LABELS_JSON=$(api -f "https://api.github.com/repos/$GH_REPO/issues/$ISSUE_NUMBER/labels?per_page=100" | jq '[.[].name]')
MATCHED_PRIORITY=$(echo "$ISSUE_LABELS_JSON" | jq -r --argjson priority "$PRIORITY_LABELS" 'map(select(. as $x | $priority | index($x) != null)) | first // ""')
HAS_REPRO=$(echo "$ISSUE_LABELS_JSON" | jq --arg l "$REPRODUCIBLE_LABEL" 'index($l) != null')
HAS_FREQ=$(echo "$ISSUE_LABELS_JSON" | jq --argjson freq "$FREQUENCY_LABELS" 'any(.[]; . as $x | $freq | index($x) != null)')
if [ -n "$MATCHED_PRIORITY" ] && [ "$HAS_REPRO" = "true" ] && [ "$HAS_FREQ" = "true" ]; then
echo "Confirmed high-frequency $MATCHED_PRIORITY, notifying"
echo "notify_reason=confirmed $MATCHED_PRIORITY" >> "$GITHUB_OUTPUT"
echo "should_notify=true" >> "$GITHUB_OUTPUT"
else
if [ -z "$MATCHED_PRIORITY" ] || [ "$HAS_REPRO" != "true" ] || [ "$HAS_FREQ" != "true" ]; then
echo "Combination not yet satisfied (priority=$MATCHED_PRIORITY, reproducible=$HAS_REPRO, frequency=$HAS_FREQ), skipping"
echo "should_notify=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# A bulk label apply emits one `labeled` event per label, so several
# runs reach this point. Creating the reaction is our atomic claim:
# one run gets 201 and notifies, the rest get 200. It's scoped to our
# own reaction, so a human's reaction can't suppress it.
REACTION_PAYLOAD=$(jq -cn --arg content "$MARKER_REACTION" '{content: $content}')
REACTION_RESPONSE=$(api -w "\n%{http_code}" -X POST \
"https://api.github.com/repos/$GH_REPO/issues/$ISSUE_NUMBER/reactions" \
-d "$REACTION_PAYLOAD")
REACTION_BODY=$(echo "$REACTION_RESPONSE" | sed '$d')
REACTION_STATUS=$(echo "$REACTION_RESPONSE" | tail -n1)
if [ "$REACTION_STATUS" = "200" ]; then
echo "First responders already notified for this issue (reaction present), skipping"
echo "should_notify=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$REACTION_STATUS" != "201" ]; then
echo "::error::Unexpected status $REACTION_STATUS creating reaction: $REACTION_BODY"
exit 1
fi
REACTION_ID=$(echo "$REACTION_BODY" | jq -r '.id')
LABELS=$(echo "$ISSUE_LABELS_JSON" | jq -r 'join(", ")')
echo "Confirmed high-frequency $MATCHED_PRIORITY, notifying"
{
echo "notify_reason=confirmed $MATCHED_PRIORITY"
echo "issue_labels=$LABELS"
echo "reaction_id=$REACTION_ID"
echo "should_notify=true"
} >> "$GITHUB_OUTPUT"
- name: Build Slack message payload
if: steps.check-label.outputs.should_notify == 'true'
env:
@ -62,10 +111,8 @@ jobs:
ISSUE_URL: ${{ github.event.issue.html_url }}
LABELED_BY: ${{ github.event.sender.login }}
NOTIFY_REASON: ${{ steps.check-label.outputs.notify_reason }}
LABELS_JSON: ${{ toJson(github.event.issue.labels.*.name) }}
LABELS: ${{ steps.check-label.outputs.issue_labels }}
run: |
LABELS=$(echo "$LABELS_JSON" | jq -r 'join(", ")')
jq -n \
--arg notify_reason "$NOTIFY_REASON" \
--arg issue_title "$ISSUE_TITLE" \
@ -108,9 +155,27 @@ jobs:
if: steps.check-label.outputs.should_notify == 'true'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_FIRST_RESPONDERS }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
REACTION_ID: ${{ steps.check-label.outputs.reaction_id }}
run: |
set -uo pipefail
release_claim() {
if [ -n "${REACTION_ID:-}" ]; then
echo "Releasing reaction claim so the notification can be retried"
curl -sS -X DELETE \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/$GH_REPO/issues/$ISSUE_NUMBER/reactions/$REACTION_ID" || true
fi
}
if [ -z "$SLACK_WEBHOOK_URL" ]; then
echo "::error::SLACK_WEBHOOK_FIRST_RESPONDERS secret is not set"
release_claim
exit 1
fi
@ -126,6 +191,7 @@ jobs:
if [ "$HTTP_STATUS" -ne 200 ]; then
echo "::error::Slack notification failed with status $HTTP_STATUS: $HTTP_BODY"
release_claim
exit 1
fi

View file

@ -4,6 +4,9 @@ on:
label:
types: [created]
permissions:
contents: read
jobs:
notify-slack:
if: >-

View file

@ -20,13 +20,14 @@ on:
permissions:
contents: read
pull-requests: read
jobs:
check-stale-prs:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: read
env:
REPO: ${{ github.repository }}
# Only surface PRs created on or after this date. Update this if the

View file

@ -5,10 +5,16 @@ on:
- cron: "0 6 * * 1,4" # Mondays and Thursdays at 6 AM UTC
workflow_dispatch:
permissions:
contents: read
jobs:
update-duplicate-magnets:
runs-on: ubuntu-latest
if: github.repository == 'zed-industries/zed'
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1

428
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -508,7 +508,7 @@ accesskit = "0.24.0"
accesskit_macos = "0.26.0"
accesskit_unix = "0.21.0"
accesskit_windows = "0.33.1"
agent-client-protocol = { version = "=1.0.1", features = ["unstable"] }
agent-client-protocol = { version = "=1.1.0", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "4c129667ce56611becdc82de6e28218c80e2e88f" }
any_vec = "0.14"
@ -543,6 +543,7 @@ aws-credential-types = { version = "1.2.8", features = [
aws-sdk-bedrockruntime = { version = "1.112.0", features = [
"behavior-version-latest",
] }
aws-sigv4 = { version = "1.4.0", features = ["http1"] }
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"
@ -594,7 +595,7 @@ emojis = "0.6.1"
env_logger = "0.11"
encoding_rs = "0.8"
exec = "0.3.1"
fancy-regex = "0.17.0"
fancy-regex = "0.18.0"
fork = "0.4.0"
futures = "0.3.32"
futures-concurrency = "7.7.1"
@ -743,6 +744,7 @@ rustls-platform-verifier = "0.5.0"
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
schemars = { version = "1.0", features = ["indexmap2"] }
seccompiler = "0.5"
semver = { version = "1.0", features = ["serde"] }
serde = { version = "1.0.221", features = ["derive", "rc"] }
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
@ -809,7 +811,7 @@ tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex",
tree-sitter-html = "0.23"
tree-sitter-jsdoc = "0.23"
tree-sitter-json = "0.24"
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
tree-sitter-md = { git = "https://github.com/zed-industries/tree-sitter-markdown", rev = "b596e737286780d7bfa9fcddceaeeb754574b352" } # fork of 9a23c1a with serialize() buffer-overflow fix; https://github.com/tree-sitter-grammars/tree-sitter-markdown/issues/243
tree-sitter-python = "0.25"
tree-sitter-regex = "0.24"
tree-sitter-ruby = "0.23"
@ -828,8 +830,8 @@ usvg = { version = "0.46.0", default-features = false }
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
vte = { version = "0.15.0", features = ["ansi"] }
walkdir = "2.5"
wasm-encoder = "0.221"
wasmparser = "0.221"
wasm-encoder = "0.252"
wasmparser = "0.252"
wasmtime = { version = "36", default-features = false, features = [
"async",
"demangle",
@ -845,7 +847,7 @@ which = "6.0.0"
wasm-bindgen = "0.2.120"
web-time = "1.1.0"
webrtc-sys = "0.3.23"
wgpu = { git = "https://github.com/zed-industries/wgpu.git", rev = "357a0c56e0070480ad9daea5d2eaa83150b79e88" }
wgpu = "29.0.4"
windows-core = "0.61"
yaml-rust2 = "0.8"
yawc = "0.2.5"
@ -1070,3 +1072,10 @@ ignored = [
"documented",
"sea-orm-macros",
]
# Dylint discovers our custom lints through this entry, so `cargo dylint --all`
# runs them without a `--path` argument. The `lints` package pins its own
# nightly toolchain (see `tooling/lints/rust-toolchain.toml`) and is kept out of
# this workspace on purpose.
[workspace.metadata.dylint]
libraries = [{ path = "tooling/lints" }]

View file

@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 13H4C2.89543 13 2 12.1046 2 11V5C2 3.89543 2.89543 3 4 3H6.17157C6.70201 3 7.21071 3.21071 7.58579 3.58579L8.41421 4.41421C8.78929 4.78929 9.29799 5 9.82843 5H12C13.1046 5 14 5.89543 14 7V7.5" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round"/>
<path d="M11.2045 14.4761V9.7034" stroke="#DCE0E5" stroke-width="1.65153" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5926 11.5119L11.2046 9.12396L8.81659 11.5119" stroke="#DCE0E5" stroke-width="1.65153" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.2045 14.4761V9.7034" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5926 11.5119L11.2046 9.12396L8.81659 11.5119" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 640 B

After

Width:  |  Height:  |  Size: 632 B

Before After
Before After

View file

@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 13H4C2.89543 13 2 12.1046 2 11V5C2 3.89543 2.89543 3 4 3H6.17157C6.70201 3 7.21071 3.21071 7.58579 3.58579L8.41421 4.41421C8.78929 4.78929 9.29799 5 9.82843 5H12C13.1046 5 14 5.89543 14 7V7.5" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round"/>
<path opacity="0.2" d="M4 13H6H12C13.1046 13 14 12.1046 14 11V7.5V7C14 5.89543 13.1046 5 12 5H9.82843C9.29799 5 8.78929 4.78929 8.41421 4.41421L7.58579 3.58579C7.21071 3.21071 6.70201 3 6.17157 3H4C2.89543 3 2 3.89543 2 5V11C2 12.1046 2.89543 13 4 13Z" fill="#DCE0E5" stroke="#DCE0E5" stroke-width="1.2"/>
<path d="M11.2045 14.4761V9.7034" stroke="#DCE0E5" stroke-width="1.65153" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5926 11.5119L11.2046 9.12396L8.81659 11.5119" stroke="#DCE0E5" stroke-width="1.65153" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.2045 14.4761V9.7034" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5926 11.5119L11.2046 9.12396L8.81659 11.5119" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 946 B

After

Width:  |  Height:  |  Size: 938 B

Before After
Before After

View file

@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="8" cy="8" r="1.25" fill="black" stroke="black" stroke-width="0.5"/>
<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 9C8.55228 9 9 8.55228 9 8C9 7.44772 8.55228 7 8 7C7.44772 7 7 7.44772 7 8C7 8.55228 7.44772 9 8 9Z" fill="#DCE0E5" stroke="#DCE0E5"/>
</svg>

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 514 B

Before After
Before After

View file

@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8H10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8H10" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 471 B

Before After
Before After

View file

@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8H10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 6V10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8H10" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 6V10" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 574 B

Before After
Before After

View file

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0295 14.8008V10.0281" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.4177 11.8366L12.0297 9.44867L9.64166 11.8366" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.66669 12.3688C1.66667 11.445 1.93321 10.5408 2.43433 9.76481C2.93545 8.98877 3.64985 8.37383 4.49181 7.99376C5.33376 7.61369 6.26751 7.48465 7.18099 7.62211C8.09447 7.75958 8.94888 8.15772 9.64169 8.76875" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.61473 7.34036C7.25075 7.49113 6.86064 7.56873 6.46668 7.56873C5.67103 7.56873 4.90796 7.25266 4.34535 6.69005C3.78275 6.12744 3.46667 5.36438 3.46667 4.56873C3.46667 3.77308 3.78275 3.01001 4.34535 2.44741C4.90796 1.8848 5.67103 1.56873 6.46668 1.56873C6.86064 1.56873 7.25075 1.64632 7.61473 1.79709C7.9787 1.94785 8.30942 2.16883 8.58799 2.44741C8.86657 2.72598 9.08755 3.0567 9.23831 3.42068C9.38908 3.78465 9.46667 4.17476 9.46667 4.56873C9.46667 4.96269 9.38908 5.3528 9.23831 5.71678C9.08755 6.08075 8.86657 6.41147 8.58799 6.69005C8.30942 6.96862 7.9787 7.1896 7.61473 7.34036Z" stroke="#DCE0E5" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -319,6 +319,7 @@
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-f": "agent::ToggleSearch",
"ctrl-alt-pageup": "agent::ScrollOutputPageUp",
"ctrl-alt-pagedown": "agent::ScrollOutputPageDown",
"ctrl-alt-home": "agent::ScrollOutputToTop",
@ -1233,6 +1234,7 @@
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
"ctrl-o": ["terminal::SendKeystroke", "ctrl-o"],
"ctrl-q": ["terminal::SendKeystroke", "ctrl-q"],
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-r": ["terminal::SendKeystroke", "ctrl-r"],
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
@ -1263,7 +1265,9 @@
},
{
"context": "AgentPanel > Terminal",
"use_key_equivalents": true,
"bindings": {
"ctrl-f": "agent::ToggleSearch",
"ctrl-n": "agent::NewThread",
},
},

View file

@ -357,6 +357,7 @@
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-f": "agent::ToggleSearch",
"ctrl-pageup": "agent::ScrollOutputPageUp",
"ctrl-pagedown": "agent::ScrollOutputPageDown",
"ctrl-home": "agent::ScrollOutputToTop",
@ -1334,6 +1335,7 @@
"context": "AgentPanel > Terminal",
"use_key_equivalents": true,
"bindings": {
"cmd-f": "agent::ToggleSearch",
"cmd-n": "agent::NewThread",
},
},

View file

@ -320,6 +320,7 @@
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-f": "agent::ToggleSearch",
"ctrl-alt-pageup": "agent::ScrollOutputPageUp",
"ctrl-alt-pagedown": "agent::ScrollOutputPageDown",
"ctrl-alt-home": "agent::ScrollOutputToTop",
@ -1278,6 +1279,7 @@
"context": "AgentPanel > Terminal",
"use_key_equivalents": true,
"bindings": {
"ctrl-f": "agent::ToggleSearch",
"ctrl-n": "agent::NewThread",
},
},

View file

@ -190,6 +190,17 @@
{ "context": "DebugPanel", "bindings": { "alt-5": "workspace::CloseActiveDock" } },
{ "context": "Diagnostics > Editor", "bindings": { "alt-6": "pane::CloseActiveItem" } },
{ "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } },
{
// `ctrl-alt-l` is bound to `editor::Format` (jetbrains "Reformat Code") in
// the Editor context above, which collides with the default
// `agent::OpenRulesLibrary` binding. Mirror the windows keymap and use
// `shift-alt-l` instead so the menu hint and the action stay in sync.
"context": "AgentPanel",
"bindings": {
"ctrl-alt-l": null,
"shift-alt-l": "agent::OpenRulesLibrary",
},
},
{
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel",
"bindings": {

View file

@ -194,6 +194,17 @@
{ "context": "DebugPanel", "bindings": { "cmd-5": "workspace::CloseActiveDock" } },
{ "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } },
{ "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } },
{
// `cmd-alt-l` is bound to `editor::Format` (jetbrains "Reformat Code") in
// the Editor context above, which collides with the default
// `agent::OpenRulesLibrary` binding. Mirror the windows keymap and use
// `shift-alt-l` instead so the menu hint and the action stay in sync.
"context": "AgentPanel",
"bindings": {
"cmd-alt-l": null,
"shift-alt-l": "agent::OpenRulesLibrary",
},
},
{
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel",
"bindings": {

View file

@ -39,6 +39,9 @@
"use_key_equivalents": true,
"bindings": {
"alt-cmd-f": "text_finder::ToProjectSearch",
"alt-cmd-[": "text_finder::Fold",
"alt-cmd-]": "text_finder::Unfold",
"cmd-shift-enter": "text_finder::ToggleFoldAll",
"cmd-j": "pane::SplitDown",
"cmd-k": "pane::SplitUp",
"cmd-h": "pane::SplitLeft",

View file

@ -39,6 +39,9 @@
"ctrl-k": "pane::SplitUp",
"ctrl-h": "pane::SplitLeft",
"ctrl-l": "pane::SplitRight",
"ctrl-{": "text_finder::Fold",
"ctrl-}": "text_finder::Unfold",
"ctrl-shift-enter": "text_finder::ToggleFoldAll",
},
},
]

View file

@ -1,33 +0,0 @@
[
// Standard macOS bindings
{
"bindings": {
"home": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"pageup": "menu::SelectFirst",
"cmd-up": "menu::SelectFirst",
"end": "menu::SelectLast",
"shift-pagedown": "menu::SelectLast",
"pagedown": "menu::SelectLast",
"cmd-down": "menu::SelectLast",
"tab": "menu::SelectNext",
"ctrl-n": "menu::SelectNext",
"down": "menu::SelectNext",
"shift-tab": "menu::SelectPrevious",
"ctrl-p": "menu::SelectPrevious",
"up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"cmd-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
"cmd-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"cmd-q": "storybook::Quit",
"backspace": "editor::Backspace",
"delete": "editor::Delete",
"left": "editor::MoveLeft",
"right": "editor::MoveRight",
},
},
]

View file

@ -73,7 +73,7 @@
"agent_buffer_font_size": 12,
// The default font size for the commit editor in the git panel and commit modal.
"git_commit_buffer_font_size": 12,
// The default font size for the markdown preview. Falls back to the editor font size if unset.
// The default font size for the markdown preview. Falls back to the UI font size if unset.
"markdown_preview_font_size": null,
// The font family for the markdown preview. Falls back to the UI font family if unset.
"markdown_preview_font_family": null,
@ -1025,11 +1025,6 @@
// Default: project_diff
"entry_primary_click_action": "project_diff",
},
"message_editor": {
// Whether to automatically replace emoji shortcodes with emoji characters.
// For example: typing `:wave:` gets replaced with `👋`.
"auto_replace_emoji_shortcode": true,
},
"agent": {
// Whether the inline assistant should use streaming tools, when available
"inline_assistant_use_streaming_tools": true,
@ -1463,7 +1458,13 @@
// The EditorConfig `end_of_line` property overrides this setting and behaves
// like `enforce_lf` or `enforce_crlf`.
"line_ending": "detect",
// Whether or not to perform a buffer format before saving: [on, off]
// Whether or not to perform a buffer format before saving:
// "on" format the whole buffer
// "off" do not format
// "modifications" format only lines with unstaged changes; skips formatting
// when no git diff is available or the language server lacks range formatting
// "modifications_if_available" same, but falls back to formatting the whole
// buffer when range formatting cannot be used
// Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored
"format_on_save": "off",
// How to perform a buffer format. This setting can take multiple values:
@ -1902,6 +1903,12 @@
"copy_on_select": false,
// Whether to keep the text selection after copying it to the clipboard.
"keep_selection_on_copy": true,
// Whether cmd-click (ctrl-click on Linux and Windows) opens hyperlinks even
// when the terminal application has enabled mouse reporting (e.g. vim with
// mouse=a, htop). When false, these clicks are forwarded to the application
// instead, and hyperlinks can still be opened with shift-cmd-click
// (shift-ctrl-click).
"open_links_in_mouse_mode": true,
// Whether to show the terminal button in the status bar
"button": true,
// Any key-value pairs added to this list will be added to the terminal's

View file

@ -224,6 +224,11 @@ pub struct SandboxFallbackAuthorizationDetails {
/// whether to run the command without a sandbox.
#[serde(default)]
pub reason: String,
/// Slug of the sandboxing docs section that best explains how to fix this
/// failure (see [`crate::LinuxWslSandboxError::docs_section`]), rendered as a
/// "Learn more" link. `None` when the cause is unknown.
#[serde(default)]
pub docs_section: Option<String>,
}
pub fn meta_with_sandbox_fallback_authorization(
@ -430,16 +435,7 @@ impl ElicitationStore {
&self.elicitations
}
fn validate_request(
request: &acp::CreateElicitationRequest,
cx: &App,
) -> Result<(), acp::Error> {
if !cx.has_flag::<AcpBetaFeatureFlag>() {
return Err(
acp::Error::invalid_params().data("elicitation support requires the ACP beta flag")
);
}
fn validate_request(request: &acp::CreateElicitationRequest) -> Result<(), acp::Error> {
if let acp::ElicitationMode::Url(mode) = &request.mode {
url::Url::parse(&mode.url)
.map_err(|_| acp::Error::invalid_params().data("invalid elicitation URL"))?;
@ -590,7 +586,7 @@ impl ElicitationStore {
request: acp::CreateElicitationRequest,
cx: &mut Context<Self>,
) -> Result<(ElicitationEntryId, Task<acp::CreateElicitationResponse>), acp::Error> {
Self::validate_request(&request, cx)?;
Self::validate_request(&request)?;
let (id, response_rx) = self.insert_pending_elicitation(request);
cx.emit(ElicitationStoreEvent::ElicitationRequested(id.clone()));
cx.notify();
@ -3485,7 +3481,7 @@ impl AcpThread {
request: acp::CreateElicitationRequest,
cx: &mut Context<Self>,
) -> Result<(ElicitationEntryId, Task<acp::CreateElicitationResponse>), acp::Error> {
ElicitationStore::validate_request(&request, cx)?;
ElicitationStore::validate_request(&request)?;
let (id, response_rx) = self.elicitations.insert_pending_elicitation(request);
self.push_entry(AgentThreadEntry::Elicitation(id.clone()), cx);
@ -4127,12 +4123,16 @@ impl AcpThread {
return Ok(());
};
let equal = git_store
let Some(equal) = git_store
.update(cx, |git, cx| {
git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
})
.await
.unwrap_or(true);
.context("failed to compare checkpoints")
.log_err()
else {
return Ok(());
};
this.update(cx, |this, cx| {
if let Some((ix, message)) = this.user_message_mut(&client_id) {
@ -4637,7 +4637,8 @@ impl AcpThread {
}
if let Some(_status) = self.pending_terminal_exit.remove(&terminal_id) {
entity.update(cx, |_term, cx| {
entity.update(cx, |term, cx| {
term.inner().update(cx, |inner, _| inner.shrink_to_used());
cx.notify();
});
}
@ -4673,7 +4674,8 @@ impl AcpThread {
status,
} => {
if let Some(entity) = self.terminals.get(&terminal_id) {
entity.update(cx, |_term, cx| {
entity.update(cx, |term, cx| {
term.inner().update(cx, |inner, _| inner.shrink_to_used());
cx.notify();
});
} else {
@ -4738,7 +4740,7 @@ mod tests {
use gpui::UpdateGlobal as _;
use gpui::{App, AsyncApp, TestAppContext, WeakEntity};
use indoc::indoc;
use project::{AgentId, FakeFs, Fs};
use project::{AgentId, FakeFs, Fs, RemoveOptions};
use rand::{distr, prelude::*};
use serde_json::json;
use settings::SettingsStore;
@ -5102,6 +5104,83 @@ mod tests {
);
}
#[gpui::test]
async fn test_terminal_exit_preserves_visible_scrollback(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let connection = Rc::new(FakeAgentConnection::new());
let thread = cx
.update(|cx| {
connection.new_session(
project,
PathList::new(&[std::path::Path::new(path!("/test"))]),
cx,
)
})
.await
.unwrap();
let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
let lower = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
cx.background_executor(),
PathStyle::local(),
);
builder.subscribe(cx)
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id.clone(),
label: "Buffered Test".to_string(),
cwd: None,
output_byte_limit: None,
terminal: lower.clone(),
},
cx,
);
});
let mut output = String::new();
for line in 0..15_000 {
output.push_str(&format!("line {line}\n"));
}
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id.clone(),
data: output.into_bytes(),
},
cx,
);
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id.clone(),
status: acp::TerminalExitStatus::new().exit_code(0),
},
cx,
);
});
let content = thread.read_with(cx, |thread, cx| {
let term = thread.terminal(terminal_id.clone()).unwrap();
term.read_with(cx, |term, cx| term.inner().read(cx).get_content())
});
assert!(
content.contains("line 14999"),
"expected output to remain visible after terminal exit, got: {content}"
);
}
#[gpui::test]
async fn test_terminal_output_and_exit_buffered_before_created(cx: &mut gpui::TestAppContext) {
init_test(cx);
@ -7333,7 +7412,7 @@ mod tests {
}
#[gpui::test]
async fn test_elicitation_requires_acp_beta_flag(cx: &mut TestAppContext) {
async fn test_elicitation_is_available_without_acp_beta_flag(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
cx.update_flags(false, vec![]);
@ -7355,8 +7434,13 @@ mod tests {
)
});
assert!(result.is_err());
thread.read_with(cx, |thread, _| assert!(thread.entries().is_empty()));
assert!(result.is_ok());
thread.read_with(cx, |thread, _| {
assert!(matches!(
thread.entries(),
[AgentThreadEntry::Elicitation(_)]
));
});
}
#[gpui::test]
@ -9175,6 +9259,84 @@ mod tests {
);
}
/// This is a regression test for a bug where update_last_checkpoint would
/// swallow a checkpoint comparison error and hide an already-visible
/// "Restore checkpoint" button without logging anything.
#[gpui::test]
async fn test_update_last_checkpoint_compare_error_keeps_checkpoint_visible(
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/test"), json!({".git": {}, "file.txt": "content"}))
.await;
let project = Project::test(fs.clone(), [Path::new(path!("/test"))], cx).await;
// The handler waits for this signal so the repository can be swapped
// out while the turn is still running.
let (complete_tx, complete_rx) = futures::channel::oneshot::channel::<()>();
let complete_rx = RefCell::new(Some(complete_rx));
let connection = Rc::new(FakeAgentConnection::new().on_user_message(
move |_, _thread, _cx| {
let complete_rx = complete_rx.borrow_mut().take();
async move {
if let Some(rx) = complete_rx {
rx.await.ok();
}
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
}
.boxed_local()
},
));
let thread = cx
.update(|cx| {
connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
})
.await
.unwrap();
let send_future = thread.update(cx, |thread, cx| thread.send_raw("message", cx));
let send_task = cx.background_executor.spawn(send_future);
cx.run_until_parked();
// Show the checkpoint, as update_last_checkpoint_if_changed does when
// files change during the turn.
thread.update(cx, |thread, _| {
let (_, message) = thread.last_user_message().unwrap();
message.checkpoint.as_mut().unwrap().show = true;
});
// Recreate `.git` so the git store reopens the repository. The fresh
// fake repository doesn't contain the checkpoint recorded at send
// time, so the end-of-turn comparison fails.
fs.remove_dir(
Path::new(path!("/test/.git")),
RemoveOptions {
recursive: true,
ignore_if_not_exists: false,
},
)
.await
.unwrap();
cx.run_until_parked();
fs.create_dir(Path::new(path!("/test/.git"))).await.unwrap();
cx.run_until_parked();
complete_tx.send(()).unwrap();
send_task.await.unwrap();
cx.run_until_parked();
thread.update(cx, |thread, _| {
let (_, message) = thread.last_user_message().unwrap();
assert!(
message.checkpoint.as_ref().unwrap().show,
"a checkpoint comparison failure must not hide the restore checkpoint button"
);
});
}
/// Tests that when a follow-up message is sent during generation,
/// the first turn completing does NOT clear `running_turn` because
/// it now belongs to the second turn.

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use chrono::{DateTime, Utc};
use collections::{HashMap, HashSet, IndexMap};
use gpui::{Entity, SharedString, Task};
use language_model::{DisabledReason, LanguageModelProviderId};
use language_model::DisabledReason;
use project::{AgentId, Project};
use serde::{Deserialize, Serialize};
use std::{any::Any, error::Error, fmt, path::PathBuf, rc::Rc};
@ -421,26 +421,17 @@ impl dyn AgentSessionList {
#[derive(Debug)]
pub struct AuthRequired {
pub description: Option<String>,
pub provider_id: Option<LanguageModelProviderId>,
}
impl AuthRequired {
pub fn new() -> Self {
Self {
description: None,
provider_id: None,
}
Self { description: None }
}
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
pub fn with_language_model_provider(mut self, provider_id: LanguageModelProviderId) -> Self {
self.provider_id = Some(provider_id);
self
}
}
impl Error for AuthRequired {}

View file

@ -177,7 +177,7 @@ impl Diff {
};
format!(
"Diff: {}\n```\n{}\n```\n",
path.unwrap_or("untitled".into()),
path.unwrap_or(MultiBuffer::DEFAULT_TITLE.into()),
buffer_text
)
}
@ -260,7 +260,7 @@ impl PendingDiff {
let path = new_buffer
.file()
.map(|file| file.path().display(file.path_style(cx)))
.unwrap_or("untitled".into())
.unwrap_or(MultiBuffer::DEFAULT_TITLE.into())
.into();
let replica_id = new_buffer.replica_id();

View file

@ -83,33 +83,6 @@ impl MentionUri {
.and_then(|input| input.strip_suffix('`'))
.unwrap_or(input);
fn parse_line_range(fragment: &str) -> Result<RangeInclusive<u32>> {
let range = fragment.strip_prefix("L").unwrap_or(fragment);
let (start, end) = if let Some((start, end)) = range.split_once(":") {
(start, end)
} else if let Some((start, end)) = range.split_once("-") {
// Also handle L10-20 or L10-L20 format
(start, end.strip_prefix("L").unwrap_or(end))
} else {
// Single line number like L1872 - treat as a range of one line
(range, range)
};
let start_line = start
.parse::<u32>()
.context("Parsing line range start")?
.checked_sub(1)
.context("Line numbers should be 1-based")?;
let end_line = end
.parse::<u32>()
.context("Parsing line range end")?
.checked_sub(1)
.context("Line numbers should be 1-based")?;
Ok(start_line..=end_line)
}
let parse_column =
|input: Option<String>| -> Option<u32> { input?.parse::<u32>().ok()?.checked_sub(1) };
let validate_query_params = |url: &Url, allowed: &[&str]| -> Result<()> {
@ -121,37 +94,6 @@ impl MentionUri {
Ok(())
};
let parse_absolute_path = |input: &str| -> Result<Self> {
let (path_input, fragment) = input
.split_once('#')
.map_or((input, None), |(path, fragment)| (path, Some(fragment)));
if let Some(fragment) = fragment.and_then(|fragment| parse_line_range(fragment).ok()) {
return Ok(MentionUri::Selection {
abs_path: Some(path_input.into()),
line_range: fragment,
column: None,
});
}
let path_with_position = PathWithPosition::parse_str(path_input);
let abs_path = path_with_position.path;
if let Some(row) = path_with_position.row {
let line = row
.checked_sub(1)
.context("Line numbers should be 1-based")?;
Ok(MentionUri::Selection {
abs_path: Some(abs_path),
line_range: line..=line,
column: path_with_position
.column
.map(|column| column.saturating_sub(1)),
})
} else {
Ok(MentionUri::File { abs_path })
}
};
if is_absolute(input, path_style) && !input.contains("://") {
return parse_absolute_path(input)
.with_context(|| format!("Invalid absolute path mention URI: {input}"));
@ -168,7 +110,10 @@ impl MentionUri {
};
let decoded = decode(trimmed).unwrap_or(Cow::Borrowed(trimmed));
let normalized: Cow<str> = if path_style.is_windows() {
Cow::Owned(decoded.replace('/', "\\"))
match to_native_windows_path(&decoded) {
Some(native) => Cow::Owned(native),
None => decoded,
}
} else {
decoded
};
@ -337,6 +282,56 @@ impl MentionUri {
}
}
/// Parses a hyperlink target from agent-authored Markdown.
///
/// Unlike [`MentionUri::parse`] — which stays strict so canonical mention
/// URIs round-trip verbatim — bare path targets are normalized first:
/// percent escapes are decoded (see [`decode_path_escapes`]) and
/// Windows-compatible spellings like `/C:/foo` or `/c/foo` become native
/// paths (see [`to_native_windows_path`]).
pub fn parse_hyperlink(input: &str, path_style: PathStyle) -> Result<Self> {
if let Some(target) = bare_path_target(input, path_style) {
return parse_hyperlink_path(target, path_style, DecodePercentEscapes::Yes)
.with_context(|| format!("Invalid hyperlink path target: {input}"));
}
Self::parse(input, path_style)
}
/// Returns the literal (un-decoded) interpretation of a bare-path
/// hyperlink target, for files whose names literally contain an escape
/// sequence (e.g. `a%20b.rs`). Returns `None` when this wouldn't differ
/// from [`MentionUri::parse_hyperlink`], including for URLs, whose
/// escapes are unambiguous.
pub fn parse_hyperlink_literal(input: &str, path_style: PathStyle) -> Option<Self> {
let target = bare_path_target(input, path_style)?;
let (path_input, _) = split_path_fragment(target);
if !matches!(decode_path_escapes(path_input), Cow::Owned(_)) {
return None;
}
parse_hyperlink_path(target, path_style, DecodePercentEscapes::No).ok()
}
/// The absolute path this mention refers to, if it refers to one.
pub fn abs_path(&self) -> Option<&Path> {
match self {
MentionUri::File { abs_path }
| MentionUri::Directory { abs_path }
| MentionUri::Symbol { abs_path, .. } => Some(abs_path),
MentionUri::Selection { abs_path, .. } => abs_path.as_deref(),
MentionUri::Skill {
skill_file_path, ..
} => Some(skill_file_path),
MentionUri::PastedImage { .. }
| MentionUri::Thread { .. }
| MentionUri::Rule { .. }
| MentionUri::Diagnostics { .. }
| MentionUri::Fetch { .. }
| MentionUri::TerminalSelection { .. }
| MentionUri::GitDiff { .. }
| MentionUri::MergeConflict { .. } => None,
}
}
pub fn name(&self) -> String {
match self {
MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => abs_path
@ -599,6 +594,217 @@ impl fmt::Display for MentionLink<'_> {
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DecodePercentEscapes {
Yes,
No,
}
fn parse_line_range(fragment: &str) -> Result<RangeInclusive<u32>> {
let range = fragment.strip_prefix("L").unwrap_or(fragment);
let (start, end) = if let Some((start, end)) = range.split_once(":") {
(start, end)
} else if let Some((start, end)) = range.split_once("-") {
// Also handle L10-20 or L10-L20 format
(start, end.strip_prefix("L").unwrap_or(end))
} else {
// Single line number like L1872 - treat as a range of one line
(range, range)
};
let start_line = start
.parse::<u32>()
.context("Parsing line range start")?
.checked_sub(1)
.context("Line numbers should be 1-based")?;
let end_line = end
.parse::<u32>()
.context("Parsing line range end")?
.checked_sub(1)
.context("Line numbers should be 1-based")?;
Ok(start_line..=end_line)
}
/// Returns the mention target as a bare absolute path (not a URL), with the
/// backticks agents sometimes add stripped.
fn bare_path_target(input: &str, path_style: PathStyle) -> Option<&str> {
let input = input
.strip_prefix('`')
.and_then(|input| input.strip_suffix('`'))
.unwrap_or(input);
(is_absolute(input, path_style) && !input.contains("://")).then_some(input)
}
fn split_path_fragment(input: &str) -> (&str, Option<&str>) {
input
.split_once('#')
.map_or((input, None), |(path, fragment)| (path, Some(fragment)))
}
fn parse_absolute_path(input: &str) -> Result<MentionUri> {
let (path_input, fragment) = split_path_fragment(input);
absolute_path_mention(path_input, fragment)
}
/// Like [`parse_absolute_path`], but normalizes hyperlink spellings first.
fn parse_hyperlink_path(
input: &str,
path_style: PathStyle,
decode_escapes: DecodePercentEscapes,
) -> Result<MentionUri> {
let (path_input, fragment) = split_path_fragment(input);
let path_input = normalize_path_mention(path_input, path_style, decode_escapes);
absolute_path_mention(&path_input, fragment)
}
fn absolute_path_mention(path_input: &str, fragment: Option<&str>) -> Result<MentionUri> {
if let Some(fragment) = fragment.and_then(|fragment| parse_line_range(fragment).ok()) {
return Ok(MentionUri::Selection {
abs_path: Some(path_input.into()),
line_range: fragment,
column: None,
});
}
let path_with_position = PathWithPosition::parse_str(path_input);
let abs_path = path_with_position.path;
if let Some(row) = path_with_position.row {
let line = row
.checked_sub(1)
.context("Line numbers should be 1-based")?;
Ok(MentionUri::Selection {
abs_path: Some(abs_path),
line_range: line..=line,
column: path_with_position
.column
.map(|column| column.saturating_sub(1)),
})
} else {
Ok(MentionUri::File { abs_path })
}
}
fn normalize_path_mention(
input: &str,
path_style: PathStyle,
decode_escapes: DecodePercentEscapes,
) -> Cow<'_, str> {
let decoded = match decode_escapes {
DecodePercentEscapes::Yes => decode_path_escapes(input),
DecodePercentEscapes::No => Cow::Borrowed(input),
};
if !path_style.is_windows() {
return decoded;
}
match to_native_windows_path(&decoded) {
Some(native) => Cow::Owned(native),
None => decoded,
}
}
/// Decodes percent escapes in a path, leaving separator escapes (`%2F`,
/// `%5C`) encoded so decoding can't change which directories the path
/// traverses. Invalid sequences and non-UTF-8 results leave the input
/// unchanged. Returns `Cow::Owned` iff decoding changed the input
/// (`parse_hyperlink_literal` relies on this).
fn decode_path_escapes(input: &str) -> Cow<'_, str> {
fn hex_digit(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
if !input.contains('%') {
return Cow::Borrowed(input);
}
let bytes = input.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'%'
&& let Some(high) = bytes.get(index + 1).copied().and_then(hex_digit)
&& let Some(low) = bytes.get(index + 2).copied().and_then(hex_digit)
{
let byte = (high << 4) | low;
if byte != b'/' && byte != b'\\' {
decoded.push(byte);
index += 3;
continue;
}
}
decoded.push(bytes[index]);
index += 1;
}
if decoded == bytes {
return Cow::Borrowed(input);
}
match String::from_utf8(decoded) {
Ok(decoded) => Cow::Owned(decoded),
Err(_) => Cow::Borrowed(input),
}
}
/// Converts Windows-compatible path spellings into a native Windows path,
/// normalizing separators to backslashes and drive letters to uppercase so
/// parsed paths compare equal to worktree paths. Returns `None` when the
/// input needs no changes.
fn to_native_windows_path(path: &str) -> Option<String> {
fn join_drive(drive: char, rest: &str) -> String {
format!(
"{}:\\{}",
drive.to_ascii_uppercase(),
rest.replace('/', "\\")
)
}
if let Some(rest) = path.strip_prefix('/') {
// URL-style path with a leading slash before the drive: `/C:/foo`.
let mut chars = rest.chars();
if let (Some(drive), Some(':'), Some('/' | '\\')) =
(chars.next(), chars.next(), chars.next())
&& drive.is_ascii_alphabetic()
{
return Some(join_drive(drive, chars.as_str()));
}
// MSYS/Git Bash style: `/c/foo`. Lowercase-only, since that's what
// those shells emit and uppercase risks misreading real directories.
let mut chars = rest.chars();
if let (Some(drive), Some('/' | '\\')) = (chars.next(), chars.next())
&& drive.is_ascii_lowercase()
{
return Some(join_drive(drive, chars.as_str()));
}
}
// A native path with a drive prefix: uppercase the drive and normalize
// separators, e.g. `c:/foo` or `c:\foo`.
let mut chars = path.chars();
if let (Some(drive), Some(':')) = (chars.next(), chars.next())
&& drive.is_ascii_alphabetic()
{
if drive.is_ascii_uppercase() && !path.contains('/') {
return None;
}
return Some(format!(
"{}:{}",
drive.to_ascii_uppercase(),
chars.as_str().replace('/', "\\")
));
}
if path.contains('/') {
return Some(path.replace('/', "\\"));
}
None
}
fn default_include_errors() -> bool {
true
}
@ -727,6 +933,298 @@ mod tests {
}
}
#[test]
fn test_parse_file_uri_with_spaces() {
let parsed =
MentionUri::parse("file:///C:/path%20with%20space/file.rs", PathStyle::Windows)
.unwrap();
match parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path, PathBuf::from("C:\\path with space\\file.rs"));
}
other => panic!("Expected File variant, got {other:?}"),
}
assert_eq!(
MentionUri::File {
abs_path: PathBuf::from("C:\\path with space\\file.rs")
}
.to_uri()
.to_string(),
"file:///C:/path%20with%20space/file.rs"
);
}
#[test]
fn test_parse_windows_drive_path_with_leading_slash_and_line() {
let parsed = MentionUri::parse_hyperlink(
"/C:/Projects/Example Workspace/Cargo.toml:2",
PathStyle::Windows,
)
.unwrap();
match parsed {
MentionUri::Selection {
abs_path: Some(abs_path),
line_range,
..
} => {
assert_eq!(
abs_path,
PathBuf::from("C:\\Projects\\Example Workspace\\Cargo.toml")
);
assert_eq!(line_range, 1..=1);
}
other => panic!("Expected Selection variant, got {other:?}"),
}
}
#[test]
fn test_parse_windows_path_with_percent_escaped_spaces_and_line() {
let parsed = MentionUri::parse_hyperlink(
"C:\\Projects\\Example%20Workspace\\path\\to\\filename.ext:42",
PathStyle::Windows,
)
.unwrap();
match parsed {
MentionUri::Selection {
abs_path: Some(abs_path),
line_range,
..
} => {
assert_eq!(
abs_path,
PathBuf::from("C:\\Projects\\Example Workspace\\path\\to\\filename.ext")
);
assert_eq!(line_range, 41..=41);
}
other => panic!("Expected Selection variant, got {other:?}"),
}
}
#[test]
fn test_parse_windows_compat_path_with_spaces() {
let parsed = MentionUri::parse_hyperlink(
"/c/Projects/Example Workspace/AGENTS.md",
PathStyle::Windows,
)
.unwrap();
match parsed {
MentionUri::File { abs_path } => {
assert_eq!(
abs_path,
PathBuf::from("C:\\Projects\\Example Workspace\\AGENTS.md")
);
}
other => panic!("Expected File variant, got {other:?}"),
}
}
#[test]
fn test_parse_windows_drive_path_with_leading_slash_and_fragment_line() {
let parsed =
MentionUri::parse_hyperlink("/C:/Projects/Cargo.toml#L4", PathStyle::Windows).unwrap();
match parsed {
MentionUri::Selection {
abs_path: Some(abs_path),
line_range,
..
} => {
assert_eq!(abs_path, PathBuf::from("C:\\Projects\\Cargo.toml"));
assert_eq!(line_range, 3..=3);
}
other => panic!("Expected Selection variant, got {other:?}"),
}
}
#[test]
fn test_windows_drive_path_with_leading_slash_round_trips() {
let parsed = MentionUri::parse_hyperlink("/C:/dir/file.rs", PathStyle::Windows).unwrap();
assert_eq!(
parsed,
MentionUri::File {
abs_path: PathBuf::from("C:\\dir\\file.rs")
}
);
let uri = parsed.to_uri().to_string();
assert_eq!(uri, "file:///C:/dir/file.rs");
assert_eq!(MentionUri::parse(&uri, PathStyle::Windows).unwrap(), parsed);
}
#[test]
fn test_parse_windows_unc_path() {
let parsed =
MentionUri::parse_hyperlink("//server/share/dir/file.rs", PathStyle::Windows).unwrap();
match parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path, PathBuf::from("\\\\server\\share\\dir\\file.rs"));
}
other => panic!("Expected File variant, got {other:?}"),
}
}
#[test]
fn test_parse_windows_drive_letters_are_uppercased() {
for input in [
"file:///c:/foo/bar.rs",
"/c:/foo/bar.rs",
"/c/foo/bar.rs",
"c:\\foo\\bar.rs",
"c:/foo/bar.rs",
] {
let parsed = MentionUri::parse_hyperlink(input, PathStyle::Windows).unwrap();
assert_eq!(
parsed,
MentionUri::File {
abs_path: PathBuf::from("C:\\foo\\bar.rs")
},
"input: {input}"
);
}
}
#[test]
fn test_msys_style_paths_require_lowercase_drive() {
// Uppercase `/C/foo` is more likely a real directory than a drive.
let parsed = MentionUri::parse_hyperlink("/C/Users/readme.md", PathStyle::Windows).unwrap();
match parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path, PathBuf::from("\\C\\Users\\readme.md"));
}
other => panic!("Expected File variant, got {other:?}"),
}
}
#[test]
fn test_posix_paths_are_not_rewritten_as_windows_drives() {
let parsed =
MentionUri::parse_hyperlink("/c/Projects/AGENTS.md", PathStyle::Posix).unwrap();
match parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path, PathBuf::from("/c/Projects/AGENTS.md"));
}
other => panic!("Expected File variant, got {other:?}"),
}
}
#[test]
fn test_hyperlink_percent_escapes_are_decoded() {
let parsed = MentionUri::parse_hyperlink("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
abs_path: PathBuf::from("/tmp/a b.rs")
}
);
// Invalid escape sequences pass through unchanged.
let parsed =
MentionUri::parse_hyperlink("C:\\dir\\100%_done.txt", PathStyle::Windows).unwrap();
assert_eq!(
parsed,
MentionUri::File {
abs_path: PathBuf::from("C:\\dir\\100%_done.txt")
}
);
// Separator escapes stay encoded (no introduced path traversal).
let parsed = MentionUri::parse_hyperlink("/tmp/a%2Fb.rs", PathStyle::Posix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
abs_path: PathBuf::from("/tmp/a%2Fb.rs")
}
);
let parsed =
MentionUri::parse_hyperlink("/tmp/..%2F..%2Fsecret", PathStyle::Posix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
abs_path: PathBuf::from("/tmp/..%2F..%2Fsecret")
}
);
}
#[test]
fn test_parse_keeps_bare_path_targets_verbatim() {
let parsed = MentionUri::parse("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
abs_path: PathBuf::from("/tmp/a%20b.rs")
}
);
let parsed = MentionUri::parse("/c/Projects/AGENTS.md", PathStyle::Windows).unwrap();
assert_eq!(
parsed,
MentionUri::File {
abs_path: PathBuf::from("/c/Projects/AGENTS.md")
}
);
}
#[test]
fn test_parse_hyperlink_literal_keeps_percent_escapes() {
let literal =
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
assert_eq!(
literal,
MentionUri::File {
abs_path: PathBuf::from("/tmp/a%20b.rs")
}
);
// Line suffixes still parse.
let literal =
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs:42", PathStyle::Posix).unwrap();
assert_eq!(
literal,
MentionUri::Selection {
abs_path: Some(PathBuf::from("/tmp/a%20b.rs")),
line_range: 41..=41,
column: None,
}
);
// Windows normalization still applies.
let literal =
MentionUri::parse_hyperlink_literal("/C:/dir/a%20b.rs", PathStyle::Windows).unwrap();
assert_eq!(
literal,
MentionUri::File {
abs_path: PathBuf::from("C:\\dir\\a%20b.rs")
}
);
}
#[test]
fn test_parse_hyperlink_literal_returns_none_when_unambiguous() {
// No percent escapes: identical to `parse_hyperlink`.
assert_eq!(
MentionUri::parse_hyperlink_literal("/tmp/a b.rs", PathStyle::Posix),
None
);
// Invalid escape sequences are also left alone by `parse_hyperlink`.
assert_eq!(
MentionUri::parse_hyperlink_literal("/tmp/100%_done.txt", PathStyle::Posix),
None
);
// Separator escapes are never decoded, so they're not ambiguous.
assert_eq!(
MentionUri::parse_hyperlink_literal("/tmp/a%2Fb.rs", PathStyle::Posix),
None
);
// URLs are spec-encoded, not ambiguous.
assert_eq!(
MentionUri::parse_hyperlink_literal("file:///tmp/a%20b.rs", PathStyle::Posix),
None
);
// Relative paths are not bare-path mentions.
assert_eq!(
MentionUri::parse_hyperlink_literal("tmp/a%20b.rs", PathStyle::Posix),
None
);
}
#[test]
fn test_to_directory_uri_without_slash() {
let uri = MentionUri::Directory {

View file

@ -129,6 +129,31 @@ impl LinuxWslSandboxError {
LinuxWslSandboxError::Other(message) => message.clone(),
}
}
/// The slug of the sandboxing docs section that best explains how to resolve
/// this failure, for deep-linking from the UI. Pair with
/// `client::zed_urls::sandboxing_docs`.
pub fn docs_section(&self) -> &'static str {
match self {
// Both "no bwrap" and "only a setuid-root bwrap" are resolved by
// installing a non-setuid Bubblewrap.
LinuxWslSandboxError::BwrapNotFound | LinuxWslSandboxError::SetuidRejected => {
"installing-bubblewrap"
}
// A failed probe on Linux is almost always disabled unprivileged
// user namespaces, which the Ubuntu-specific section covers.
LinuxWslSandboxError::SandboxProbeFailed => "installing-bubblewrap-ubuntu",
// Catch-all (includes WSL/Windows messages): point at the platform
// overview for the current OS.
LinuxWslSandboxError::Other(_) => {
if cfg!(target_os = "windows") {
"windows"
} else {
"linux"
}
}
}
}
}
impl SandboxWrap {
@ -151,6 +176,15 @@ impl SandboxWrap {
/// grant as a [`sandbox::HostFilesystemLocation`] (pinning the inode / canonical
/// path) rather than passing a re-resolvable path. A location that can't be
/// captured (e.g. it doesn't exist) is dropped from the grant — fail-closed.
///
/// This function has **no filesystem side effects**: it never creates paths.
/// It is used both by the side-effect-free [`Self::can_create_sandbox`] probe
/// and by real sandbox construction, and must behave identically. On Linux a
/// writable grant that doesn't exist yet simply can't be captured (bwrap
/// can't bind a missing path), so it's dropped here — the sanctioned way to
/// get a grant to a new directory is the `create_directory` tool, which
/// creates it (pinning the inode) before the grant is recorded. On macOS a
/// missing leaf still canonicalizes, so such grants are captured directly.
fn to_policy(&self) -> sandbox::SandboxPolicy {
let protected_paths = self
.protected_paths
@ -164,12 +198,11 @@ impl SandboxWrap {
.writable_paths
.iter()
.chain(self.extra_write_paths.iter())
.filter_map(|path| {
// Create not-yet-existing writable grants (e.g. an approved
// scratch dir) so they can be captured and bound; best-effort.
let _ = std::fs::create_dir_all(path);
sandbox::HostFilesystemLocation::new(path).ok()
})
// Capture only — never create anything here (see the doc comment):
// materializing an approved-but-missing grant is deferred to
// `Sandbox::new` so it can never happen during the `can_create`
// probe, before the user has approved the grant.
.filter_map(|path| sandbox::HostFilesystemLocation::new(path).ok())
.collect();
sandbox::SandboxFsPolicy::Restricted {
writable_paths,

View file

@ -374,7 +374,10 @@ impl LanguageModels {
}
}
cx.update(language_models::update_environment_fallback_model);
cx.update(|cx| {
LanguageModelRegistry::global(cx)
.update(cx, |registry, cx| registry.refresh_fallback_model(cx))
});
})
}
}

View file

@ -2,7 +2,7 @@ use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent};
use acp_thread::ClientUserMessageId;
use agent_client_protocol::schema::v1 as acp;
use agent_settings::AgentProfileId;
use anyhow::{Result, anyhow};
use anyhow::Result;
use chrono::{DateTime, Utc};
use collections::{HashMap, IndexMap};
use futures::{FutureExt, future::Shared};
@ -281,7 +281,9 @@ impl DbThread {
name: tool_use.name.into(),
raw_input: serde_json::to_string(&tool_use.input)
.unwrap_or_default(),
input: tool_use.input,
input: language_model::LanguageModelToolUseInput::Json(
tool_use.input,
),
is_input_complete: true,
thought_signature: None,
},
@ -444,7 +446,7 @@ impl ThreadsDatabase {
data BLOB NOT NULL
)
"})?()
.map_err(|e| anyhow!("Failed to create threads table: {}", e))?;
.map_err(|e| e.context("Failed to create threads table"))?;
if let Ok(mut s) = connection.exec(indoc! {"
ALTER TABLE threads ADD COLUMN parent_id TEXT

View file

@ -5,10 +5,9 @@
//! caller see the same answer (and so the `target_os` gate lives in one
//! place instead of scattered across the agent crate).
//!
//! The current policy is: enabled iff the user has the `sandboxing` feature
//! flag turned on, the project is local, the platform has an integration, and
//! the user has not persistently allowed unsandboxed execution (the
//! `allow_unsandboxed` sandbox setting). Setting `allow_unsandboxed`
//! The current policy is: enabled iff the project is local, the platform has an
//! integration, and the user has not persistently allowed unsandboxed execution
//! (the `allow_unsandboxed` sandbox setting). Setting `allow_unsandboxed`
//! persistently turns sandboxing off for the model-facing surface entirely:
//! the plain (non-sandboxed) `terminal` tool is exposed and the system prompt
//! omits the sandbox section, since every command would run without a wrap
@ -19,14 +18,12 @@
//!
//! macOS (Seatbelt), Linux (Bubblewrap), and Windows (Bubblewrap via WSL)
//! have real sandbox integrations; on platforms without one the per-command
//! wrap is a no-op, so commands run with the agent's ambient permissions even
//! when the flag is on.
//! wrap is a no-op, so commands run with the agent's ambient permissions.
//!
//! Naming note: this module is about agent terminal sandboxing specifically.
//! Other agent operations (e.g. file edits) are gated separately.
use agent_settings::{AgentSettings, SandboxPermissions};
use feature_flags::{FeatureFlagAppExt as _, SandboxingFeatureFlag};
use gpui::App;
use http_proxy::HostPattern;
use project::Project;
@ -176,12 +173,6 @@ pub fn settings_sandbox_policy(persistent: &SandboxPermissions) -> SandboxPolicy
SandboxPolicy { fs, network }
}
/// Whether agent-run terminal commands should be wrapped in an OS-level
/// sandbox for this process. See module docs for the policy.
pub(crate) fn sandboxing_enabled(cx: &App) -> bool {
cx.has_flag::<SandboxingFeatureFlag>()
}
/// Whether the sandboxed terminal can be exposed for this project.
///
/// The persistent `allow_unsandboxed` setting turns sandboxing off for the
@ -193,20 +184,19 @@ pub(crate) fn sandboxing_enabled(cx: &App) -> bool {
/// prompt in place, since the model is still operating in the sandbox model and
/// only escaping individual commands (tracked in `ThreadSandboxGrants`).
pub(crate) fn sandboxing_enabled_for_project(project: &Project, cx: &App) -> bool {
sandboxing_available_for_project(project, cx)
sandboxing_available_for_project(project)
&& !AgentSettings::get_global(cx)
.sandbox_permissions
.allow_unsandboxed
}
/// Whether sandboxing is *applicable* for this project at all — the feature is
/// enabled, the project is local, and the platform has a sandbox integration —
/// independent of the persistent `allow_unsandboxed` setting. Used by the UI to
/// distinguish "sandboxing isn't relevant here" (don't show the indicator) from
/// "sandboxing is available but turned off in settings" (show it, struck out).
pub(crate) fn sandboxing_available_for_project(project: &Project, cx: &App) -> bool {
sandboxing_enabled(cx)
&& project.is_local()
/// Whether sandboxing is *applicable* for this project at all — the project is
/// local and the platform has a sandbox integration — independent of the
/// persistent `allow_unsandboxed` setting. Used by the UI to distinguish
/// "sandboxing isn't relevant here" (don't show the indicator) from "sandboxing
/// is available but turned off in settings" (show it, struck out).
pub(crate) fn sandboxing_available_for_project(project: &Project) -> bool {
project.is_local()
&& cfg!(any(
target_os = "macos",
target_os = "linux",

View file

@ -198,7 +198,7 @@ You can request elevated permissions on individual `terminal` calls:
- `allow_hosts: ["github.com", "*.npmjs.org"]` — allow outbound HTTP/HTTPS to specific hosts (exact hostnames or leading-`*.` subdomain wildcards; no IP literals). Prefer this whenever you know which hosts the command needs.
- `allow_all_hosts: true` — lift the network restriction entirely: outbound access to any host over any protocol, so SSH, FTP, and raw sockets work too (unlike `allow_hosts`, which is HTTP/HTTPS-only). Use only when the specific hosts can't be enumerated up front.
{{/if}}
- `fs_write_paths: ["/abs/or/worktree-relative/path", ...]` — allow writes to specific paths (each directory grants its whole subtree). Prefer this whenever you know which paths the command needs to write. Git metadata paths cannot be requested and will never be made writable while sandboxed.
- `fs_write_paths: ["/abs/or/worktree-relative/path", ...]` — allow writes to specific paths (each directory grants its whole subtree). Prefer this whenever you know which paths the command needs to write. Each path must be an existing directory. To write into a directory that doesn't exist yet, first create it with the `create_directory` tool (which creates it and grants write access to exactly that directory) rather than requesting write access to a broad existing parent. Git metadata paths cannot be requested and will never be made writable while sandboxed.
- `allow_fs_write_all: true` — allow unrestricted filesystem writes except protected Git metadata. Only use this when the specific paths can't be enumerated up front.
- `unsandboxed: true` — run the command with no sandbox at all. Use only when none of the above suffice, including when a command must write Git metadata.

View file

@ -689,7 +689,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
id: "tool_1".into(),
name: EchoTool::NAME.into(),
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
is_input_complete: true,
thought_signature: None,
};
@ -840,17 +840,25 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) {
assert_eq!(last_tool_use.name.as_ref(), "word_list");
if tool_call.status == acp::ToolCallStatus::Pending {
if !last_tool_use.is_input_complete
&& last_tool_use.input.get("g").is_none()
&& last_tool_use
.input
.as_json()
.and_then(|input| input.get("g"))
.is_none()
{
saw_partial_tool_use = true;
}
} else {
last_tool_use
.input
.as_json()
.expect("tool input should be JSON")
.get("a")
.expect("'a' has streamed because input is now complete");
last_tool_use
.input
.as_json()
.expect("tool input should be JSON")
.get("g")
.expect("'g' has streamed because input is now complete");
}
@ -884,7 +892,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
id: "tool_id_1".into(),
name: ToolRequiringPermission::NAME.into(),
raw_input: "{}".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -894,7 +902,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
id: "tool_id_2".into(),
name: ToolRequiringPermission::NAME.into(),
raw_input: "{}".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -951,7 +959,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
id: "tool_id_3".into(),
name: ToolRequiringPermission::NAME.into(),
raw_input: "{}".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -990,7 +998,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
id: "tool_id_4".into(),
name: ToolRequiringPermission::NAME.into(),
raw_input: "{}".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -1029,7 +1037,7 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
id: "tool_id_1".into(),
name: "nonexistent_tool".into(),
raw_input: "{}".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -1533,7 +1541,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
id: "tool_1".into(),
name: "echo".into(),
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
is_input_complete: true,
thought_signature: None,
},
@ -1577,7 +1585,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
id: "tool_2".into(),
name: "test_server_echo".into(),
raw_input: json!({"text": "mcp"}).to_string(),
input: json!({"text": "mcp"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "mcp"})),
is_input_complete: true,
thought_signature: None,
},
@ -1587,7 +1595,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
id: "tool_3".into(),
name: "echo".into(),
raw_input: json!({"text": "native"}).to_string(),
input: json!({"text": "native"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "native"})),
is_input_complete: true,
thought_signature: None,
},
@ -1697,7 +1705,7 @@ async fn test_mcp_tool_names_are_sanitized_for_providers(cx: &mut TestAppContext
id: "tool_1".into(),
name: "snake_case_PascalCase".into(),
raw_input: json!({}).to_string(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -1786,7 +1794,7 @@ async fn test_mcp_tool_multi_content_response(cx: &mut TestAppContext) {
id: "tool_1".into(),
name: "screenshot".into(),
raw_input: json!({}).to_string(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -1936,7 +1944,9 @@ async fn test_mcp_tool_result_displayed_when_server_disconnected(cx: &mut TestAp
name: "issue_read".into(),
raw_input: json!({"issue_url": "https://github.com/zed-industries/zed/issues/47404"})
.to_string(),
input: json!({"issue_url": "https://github.com/zed-industries/zed/issues/47404"}),
input: language_model::LanguageModelToolUseInput::Json(
json!({"issue_url": "https://github.com/zed-industries/zed/issues/47404"}),
),
is_input_complete: true,
thought_signature: None,
},
@ -2351,7 +2361,9 @@ async fn test_terminal_tool_cancellation_captures_output(cx: &mut TestAppContext
id: "terminal_tool_1".into(),
name: TerminalTool::NAME.into(),
raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
input: json!({"command": "sleep 1000", "cd": "."}),
input: language_model::LanguageModelToolUseInput::Json(
json!({"command": "sleep 1000", "cd": "."}),
),
is_input_complete: true,
thought_signature: None,
},
@ -2446,7 +2458,7 @@ async fn test_cancellation_aware_tool_responds_to_cancellation(cx: &mut TestAppC
id: "cancellation_aware_1".into(),
name: "cancellation_aware".into(),
raw_input: r#"{}"#.into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -2632,7 +2644,9 @@ async fn test_truncate_while_terminal_tool_running(cx: &mut TestAppContext) {
id: "terminal_tool_1".into(),
name: TerminalTool::NAME.into(),
raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
input: json!({"command": "sleep 1000", "cd": "."}),
input: language_model::LanguageModelToolUseInput::Json(
json!({"command": "sleep 1000", "cd": "."}),
),
is_input_complete: true,
thought_signature: None,
},
@ -2697,7 +2711,9 @@ async fn test_cancel_multiple_concurrent_terminal_tools(cx: &mut TestAppContext)
id: "terminal_tool_1".into(),
name: TerminalTool::NAME.into(),
raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
input: json!({"command": "sleep 1000", "cd": "."}),
input: language_model::LanguageModelToolUseInput::Json(
json!({"command": "sleep 1000", "cd": "."}),
),
is_input_complete: true,
thought_signature: None,
},
@ -2707,7 +2723,9 @@ async fn test_cancel_multiple_concurrent_terminal_tools(cx: &mut TestAppContext)
id: "terminal_tool_2".into(),
name: TerminalTool::NAME.into(),
raw_input: r#"{"command": "sleep 2000", "cd": "."}"#.into(),
input: json!({"command": "sleep 2000", "cd": "."}),
input: language_model::LanguageModelToolUseInput::Json(
json!({"command": "sleep 2000", "cd": "."}),
),
is_input_complete: true,
thought_signature: None,
},
@ -2811,7 +2829,9 @@ async fn test_terminal_tool_stopped_via_terminal_card_button(cx: &mut TestAppCon
id: "terminal_tool_1".into(),
name: TerminalTool::NAME.into(),
raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
input: json!({"command": "sleep 1000", "cd": "."}),
input: language_model::LanguageModelToolUseInput::Json(
json!({"command": "sleep 1000", "cd": "."}),
),
is_input_complete: true,
thought_signature: None,
},
@ -2907,7 +2927,9 @@ async fn test_terminal_tool_timeout_expires(cx: &mut TestAppContext) {
id: "terminal_tool_1".into(),
name: TerminalTool::NAME.into(),
raw_input: r#"{"command": "sleep 1000", "cd": ".", "timeout_ms": 100}"#.into(),
input: json!({"command": "sleep 1000", "cd": ".", "timeout_ms": 100}),
input: language_model::LanguageModelToolUseInput::Json(
json!({"command": "sleep 1000", "cd": ".", "timeout_ms": 100}),
),
is_input_complete: true,
thought_signature: None,
},
@ -3376,7 +3398,7 @@ async fn test_cumulative_token_usage(cx: &mut TestAppContext) {
id: "tool_1".into(),
name: EchoTool::NAME.into(),
raw_input: json!({"text": "hello"}).to_string(),
input: json!({"text": "hello"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
is_input_complete: true,
thought_signature: None,
},
@ -3786,7 +3808,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
id: "tool_id_1".into(),
name: ToolRequiringPermission::NAME.into(),
raw_input: "{}".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
};
@ -3794,7 +3816,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
id: "tool_id_2".into(),
name: EchoTool::NAME.into(),
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
is_input_complete: true,
thought_signature: None,
};
@ -3992,7 +4014,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
id: "1".into(),
name: EchoTool::NAME.into(),
raw_input: input.to_string(),
input,
input: language_model::LanguageModelToolUseInput::Json(input),
is_input_complete: false,
thought_signature: None,
},
@ -4005,7 +4027,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
id: "1".into(),
name: "echo".into(),
raw_input: input.to_string(),
input,
input: language_model::LanguageModelToolUseInput::Json(input),
is_input_complete: true,
thought_signature: None,
},
@ -4175,7 +4197,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
id: "tool_1".into(),
name: EchoTool::NAME.into(),
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
is_input_complete: true,
thought_signature: None,
};
@ -4323,7 +4345,7 @@ async fn test_streaming_tool_completes_when_llm_stream_ends_without_final_input(
id: "tool_1".into(),
name: "streaming_echo".into(),
raw_input: r#"{"text": "partial"}"#.into(),
input: json!({"text": "partial"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "partial"})),
is_input_complete: false,
thought_signature: None,
};
@ -4428,7 +4450,7 @@ async fn test_streaming_tool_json_parse_error_is_forwarded_to_running_tool(
id: "tool_1".into(),
name: StreamingJsonErrorContextTool::NAME.into(),
raw_input: r#"{"text": "partial"#.into(),
input: json!({"text": "partial"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "partial"})),
is_input_complete: false,
thought_signature: None,
};
@ -5228,7 +5250,9 @@ async fn test_subagent_tool_call_end_to_end(cx: &mut TestAppContext) {
id: "subagent_1".into(),
name: SpawnAgentTool::NAME.into(),
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
input: serde_json::to_value(&subagent_tool_input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(&subagent_tool_input).unwrap(),
),
is_input_complete: true,
thought_signature: None,
};
@ -5362,7 +5386,9 @@ async fn test_subagent_tool_output_does_not_include_thinking(cx: &mut TestAppCon
id: "subagent_1".into(),
name: SpawnAgentTool::NAME.into(),
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
input: serde_json::to_value(&subagent_tool_input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(&subagent_tool_input).unwrap(),
),
is_input_complete: true,
thought_signature: None,
};
@ -5509,7 +5535,9 @@ async fn test_subagent_tool_call_cancellation_during_task_prompt(cx: &mut TestAp
id: "subagent_1".into(),
name: SpawnAgentTool::NAME.into(),
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
input: serde_json::to_value(&subagent_tool_input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(&subagent_tool_input).unwrap(),
),
is_input_complete: true,
thought_signature: None,
};
@ -5638,7 +5666,9 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) {
id: "subagent_1".into(),
name: SpawnAgentTool::NAME.into(),
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
input: serde_json::to_value(&subagent_tool_input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(&subagent_tool_input).unwrap(),
),
is_input_complete: true,
thought_signature: None,
};
@ -5699,7 +5729,9 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) {
id: "subagent_2".into(),
name: SpawnAgentTool::NAME.into(),
raw_input: serde_json::to_string(&resume_tool_input).unwrap(),
input: serde_json::to_value(&resume_tool_input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(&resume_tool_input).unwrap(),
),
is_input_complete: true,
thought_signature: None,
};
@ -6285,7 +6317,9 @@ async fn test_subagent_context_window_warning(cx: &mut TestAppContext) {
id: "subagent_1".into(),
name: SpawnAgentTool::NAME.into(),
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
input: serde_json::to_value(&subagent_tool_input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(&subagent_tool_input).unwrap(),
),
is_input_complete: true,
thought_signature: None,
};
@ -6410,7 +6444,9 @@ async fn test_subagent_no_context_window_warning_when_already_at_warning(cx: &mu
id: "subagent_1".into(),
name: SpawnAgentTool::NAME.into(),
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
input: serde_json::to_value(&subagent_tool_input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(&subagent_tool_input).unwrap(),
),
is_input_complete: true,
thought_signature: None,
};
@ -6476,7 +6512,9 @@ async fn test_subagent_no_context_window_warning_when_already_at_warning(cx: &mu
id: "subagent_2".into(),
name: SpawnAgentTool::NAME.into(),
raw_input: serde_json::to_string(&resume_tool_input).unwrap(),
input: serde_json::to_value(&resume_tool_input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(&resume_tool_input).unwrap(),
),
is_input_complete: true,
thought_signature: None,
};
@ -6583,7 +6621,9 @@ async fn test_subagent_error_propagation(cx: &mut TestAppContext) {
id: "subagent_1".into(),
name: SpawnAgentTool::NAME.into(),
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
input: serde_json::to_value(&subagent_tool_input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(&subagent_tool_input).unwrap(),
),
is_input_complete: true,
thought_signature: None,
};
@ -7350,6 +7390,194 @@ async fn test_fetch_tool_unsandboxed_lifts_restrictions(cx: &mut TestAppContext)
);
}
/// A granted host that redirects to a loopback target must not have that
/// redirect followed: loopback hosts can't be granted individually, so the hop
/// is refused just like a direct loopback fetch. This is the redirect variant of
/// the SSRF protection — the approved domain can't be used to bounce the request
/// onto the local machine.
#[gpui::test]
async fn test_fetch_tool_refuses_redirect_to_loopback(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
settings.tool_permissions.tools.insert(
FetchTool::NAME.into(),
agent_settings::ToolRules {
default: Some(settings::ToolPermissionMode::Allow),
always_allow: vec![],
always_deny: vec![],
always_confirm: vec![],
invalid_patterns: vec![],
},
);
settings
.sandbox_permissions
.network_hosts
.push("example.com".into());
agent_settings::AgentSettings::override_global(settings, cx);
});
let http_client = gpui::http_client::FakeHttpClient::create(|req| async move {
let uri = req.uri().to_string();
assert!(
uri.contains("example.com"),
"the loopback redirect target must never be requested, but saw {uri}"
);
Ok(gpui::http_client::Response::builder()
.status(302)
.header("location", "http://localhost:3000/internal")
.body("".into())
.unwrap())
});
#[allow(clippy::arc_with_non_send_sync)]
let tool = Arc::new(crate::FetchTool::new(http_client));
let (event_stream, _rx) = crate::ToolCallEventStream::test();
let input: crate::FetchToolInput =
serde_json::from_value(json!({"url": "https://example.com/start"})).unwrap();
let task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
let result = task.await;
assert!(
result.is_err(),
"expected a redirect to a loopback host to be refused"
);
assert!(
result.unwrap_err().contains("unsandboxed"),
"error should point at unsandboxed access as the way to reach loopback hosts"
);
}
/// A granted host that redirects to a *different*, ungranted host triggers a
/// fresh per-host authorization prompt for the redirect target — the redirect is
/// not silently followed to a host the user never approved.
#[gpui::test]
async fn test_fetch_tool_reauthorizes_redirect_to_new_host(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
settings.tool_permissions.tools.insert(
FetchTool::NAME.into(),
agent_settings::ToolRules {
default: Some(settings::ToolPermissionMode::Allow),
always_allow: vec![],
always_deny: vec![],
always_confirm: vec![],
invalid_patterns: vec![],
},
);
settings
.sandbox_permissions
.network_hosts
.push("example.com".into());
agent_settings::AgentSettings::override_global(settings, cx);
});
let http_client = gpui::http_client::FakeHttpClient::create(|req| async move {
let uri = req.uri().to_string();
assert!(
uri.contains("example.com"),
"the ungranted redirect target must not be requested before authorization, \
but saw {uri}"
);
Ok(gpui::http_client::Response::builder()
.status(302)
.header("location", "https://redirect-target.example/landing")
.body("".into())
.unwrap())
});
#[allow(clippy::arc_with_non_send_sync)]
let tool = Arc::new(crate::FetchTool::new(http_client));
let (event_stream, mut rx) = crate::ToolCallEventStream::test();
let input: crate::FetchToolInput =
serde_json::from_value(json!({"url": "https://example.com/start"})).unwrap();
let _task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
cx.run_until_parked();
let authorization = rx.expect_authorization().await;
let details =
acp_thread::sandbox_authorization_details_from_meta(&authorization.tool_call.meta)
.expect("a redirect to an ungranted host should request a sandbox network grant");
assert_eq!(
details.network_hosts,
vec!["redirect-target.example".to_string()]
);
assert!(!details.network_all_hosts);
}
/// Redirects between paths on an already-granted host are followed without any
/// additional prompt, so ordinary redirects (http→https upgrades, trailing-slash
/// canonicalization, etc.) keep working after the per-hop authorization change.
#[gpui::test]
async fn test_fetch_tool_follows_same_host_redirect(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
settings.tool_permissions.tools.insert(
FetchTool::NAME.into(),
agent_settings::ToolRules {
default: Some(settings::ToolPermissionMode::Allow),
always_allow: vec![],
always_deny: vec![],
always_confirm: vec![],
invalid_patterns: vec![],
},
);
settings
.sandbox_permissions
.network_hosts
.push("example.com".into());
agent_settings::AgentSettings::override_global(settings, cx);
});
let http_client = gpui::http_client::FakeHttpClient::create(|req| async move {
let uri = req.uri().to_string();
if uri.ends_with("/start") {
Ok(gpui::http_client::Response::builder()
.status(302)
.header("location", "https://example.com/final")
.body("".into())
.unwrap())
} else if uri.ends_with("/final") {
Ok(gpui::http_client::Response::builder()
.status(200)
.header("content-type", "text/plain")
.body("final content".into())
.unwrap())
} else {
panic!("unexpected request to {uri}");
}
});
#[allow(clippy::arc_with_non_send_sync)]
let tool = Arc::new(crate::FetchTool::new(http_client));
let (event_stream, mut rx) = crate::ToolCallEventStream::test();
let input: crate::FetchToolInput =
serde_json::from_value(json!({"url": "https://example.com/start"})).unwrap();
let task = cx.update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx));
let result = task.await;
assert_eq!(
result.expect("same-host redirect should succeed"),
"final content"
);
let event = rx.try_recv();
assert!(
!matches!(event, Ok(Ok(ThreadEvent::ToolCallAuthorization(_)))),
"expected no authorization prompt for a redirect to an already-granted host"
);
}
/// Approving one pending tool call with "Always for <tool>" auto-resolves
/// sibling pending authorizations for the same tool in the same turn.
#[gpui::test]
@ -7372,7 +7600,7 @@ async fn test_always_allow_resolves_pending_authorizations(cx: &mut TestAppConte
id: id.into(),
name: ToolRequiringPermission::NAME.into(),
raw_input: "{}".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -7451,7 +7679,7 @@ async fn test_external_settings_edit_resolves_pending_authorization(cx: &mut Tes
id: "tool_id_1".into(),
name: ToolRequiringPermission::NAME.into(),
raw_input: "{}".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -7522,7 +7750,7 @@ async fn test_external_deny_rule_resolves_pending_authorization(cx: &mut TestApp
id: "tool_id_1".into(),
name: ToolRequiringPermission::NAME.into(),
raw_input: "{}".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -7598,7 +7826,7 @@ async fn test_unrelated_settings_change_does_not_resolve_pending_authorization(
id: "tool_id_1".into(),
name: ToolRequiringPermission::NAME.into(),
raw_input: "{}".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -7668,7 +7896,7 @@ async fn test_always_allow_does_not_resolve_unrelated_tool_authorization(cx: &mu
id: id.into(),
name: name.into(),
raw_input: "{}".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
},
@ -7765,7 +7993,7 @@ async fn test_queued_message_ends_turn_at_boundary(cx: &mut TestAppContext) {
id: "tool_1".into(),
name: "echo".into(),
raw_input: r#"{"text": "hello"}"#.into(),
input: json!({"text": "hello"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
is_input_complete: true,
thought_signature: None,
},
@ -7847,7 +8075,7 @@ async fn test_queued_message_does_not_end_turn_without_boundary_flag(cx: &mut Te
id: "tool_1".into(),
name: "echo".into(),
raw_input: r#"{"text": "hello"}"#.into(),
input: json!({"text": "hello"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
is_input_complete: true,
thought_signature: None,
},
@ -7914,7 +8142,7 @@ async fn test_streaming_tool_error_breaks_stream_loop_immediately(cx: &mut TestA
id: "call_1".into(),
name: StreamingFailingEchoTool::NAME.into(),
raw_input: "hello".into(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: false,
thought_signature: None,
};
@ -7996,7 +8224,7 @@ async fn test_streaming_tool_error_waits_for_prior_tools_to_complete(cx: &mut Te
id: "call_1".into(),
name: StreamingEchoTool::NAME.into(),
raw_input: "hello".into(),
input: json!({ "text": "hello" }),
input: language_model::LanguageModelToolUseInput::Json(json!({ "text": "hello" })),
is_input_complete: false,
thought_signature: None,
},
@ -8005,7 +8233,7 @@ async fn test_streaming_tool_error_waits_for_prior_tools_to_complete(cx: &mut Te
id: "call_1".into(),
name: StreamingEchoTool::NAME.into(),
raw_input: "hello world".into(),
input: json!({ "text": "hello world" }),
input: language_model::LanguageModelToolUseInput::Json(json!({ "text": "hello world" })),
is_input_complete: true,
thought_signature: None,
};
@ -8015,7 +8243,7 @@ async fn test_streaming_tool_error_waits_for_prior_tools_to_complete(cx: &mut Te
let second_tool_use = LanguageModelToolUse {
name: StreamingFailingEchoTool::NAME.into(),
raw_input: "hello".into(),
input: json!({ "text": "hello" }),
input: language_model::LanguageModelToolUseInput::Json(json!({ "text": "hello" })),
is_input_complete: false,
thought_signature: None,
id: "call_2".into(),
@ -8144,7 +8372,7 @@ async fn test_mid_turn_model_and_settings_refresh(cx: &mut TestAppContext) {
id: "tool_1".into(),
name: "echo".into(),
raw_input: r#"{"text":"hello"}"#.into(),
input: json!({"text": "hello"}),
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
is_input_complete: true,
thought_signature: None,
},

View file

@ -35,7 +35,8 @@ use futures::{
};
use futures::{StreamExt, stream};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
App, AppContext, AsyncApp, Context, Entity, EventEmitter, ReadGlobal as _, SharedString, Task,
WeakEntity,
};
use heck::ToSnakeCase as _;
use language_model::{
@ -606,7 +607,7 @@ impl AgentMessage {
"{}\n",
MarkdownCodeBlock {
tag: "json",
text: &format!("{:#}", tool_use.input)
text: &format!("{:#}", tool_use.input.to_display_json())
}
));
}
@ -1181,7 +1182,7 @@ enum CompletionError {
Other(#[from] anyhow::Error),
}
pub(crate) enum ThreadModel {
pub enum ThreadModel {
Ready(Arc<dyn LanguageModel>),
Unresolved(SelectedModel),
Unset,
@ -1357,7 +1358,11 @@ impl Thread {
.and_then(|model| model.speed);
let (prompt_capabilities_tx, prompt_capabilities_rx) =
watch::channel(Self::prompt_capabilities(model.as_deref()));
let model = model.map_or(ThreadModel::Unset, ThreadModel::Ready);
let model = match model {
Some(model) => ThreadModel::Ready(model),
None => Self::user_configured_model_selection(cx)
.map_or(ThreadModel::Unset, ThreadModel::Unresolved),
};
Self {
id: acp::SessionId::new(uuid::Uuid::new_v4().to_string()),
prompt_id: PromptId::new(),
@ -1588,7 +1593,7 @@ impl Thread {
.unbounded_send(Ok(ThreadEvent::ToolCall(
acp::ToolCall::new(tool_use.id.to_string(), tool_use.name.to_string())
.status(status)
.raw_input(tool_use.input.clone()),
.raw_input(tool_use.input.to_display_json()),
)))
.ok();
let mut fields = acp::ToolCallUpdateFields::new()
@ -1601,15 +1606,12 @@ impl Thread {
return;
};
let title = tool.initial_title(tool_use.input.clone(), cx);
let Ok(input) = tool_use.input.clone().into_json() else {
return;
};
let title = tool.initial_title(input.clone(), cx);
let kind = tool.kind();
stream.send_tool_call(
&tool_use.id,
&tool_use.name,
title,
kind,
tool_use.input.clone(),
);
stream.send_tool_call(&tool_use.id, &tool_use.name, title, kind, input.clone());
if let Some(content) = replay_content {
stream.update_tool_call_fields(
@ -1630,8 +1632,7 @@ impl Thread {
self.sandbox_grants.clone(),
Some(cx.weak_entity()),
);
tool.replay(tool_use.input.clone(), output, tool_event_stream, cx)
.log_err();
tool.replay(input, output, tool_event_stream, cx).log_err();
}
stream.update_tool_call_fields(
@ -1845,12 +1846,12 @@ impl Thread {
sandboxing_enabled_for_project(self.project.read(cx), cx)
}
/// Whether sandboxing is *applicable* for this thread's project (feature on,
/// local project, supported platform), regardless of whether it's been
/// turned off in settings. The UI shows the sandbox indicator whenever this
/// is true, drawing it struck-out when sandboxing is disabled.
/// Whether sandboxing is *applicable* for this thread's project (local
/// project, supported platform), regardless of whether it's been turned off
/// in settings. The UI shows the sandbox indicator whenever this is true,
/// drawing it struck-out when sandboxing is disabled.
pub fn sandboxing_available(&self, cx: &App) -> bool {
sandboxing_available_for_project(self.project.read(cx), cx)
sandboxing_available_for_project(self.project.read(cx))
}
/// The directory subtrees the sandbox always grants write access to for this
@ -1946,6 +1947,10 @@ impl Thread {
self.model.as_model()
}
pub fn thread_model(&self) -> &ThreadModel {
&self.model
}
pub(crate) fn ensure_model(
&mut self,
default_model: Option<&Arc<dyn LanguageModel>>,
@ -1977,6 +1982,7 @@ impl Thread {
cx.emit(TokenUsageUpdated(new_usage));
}
self.prompt_capabilities_tx.send(new_caps).log_err();
cx.emit(ModelChanged);
for subagent in &self.running_subagents {
subagent
@ -2395,6 +2401,20 @@ impl Thread {
Self::resolve_model_from_selection(&selection, cx)
}
fn user_configured_model_selection(cx: &App) -> Option<SelectedModel> {
let selection = SettingsStore::global(cx)
.raw_user_settings()?
.content
.agent
.as_ref()?
.default_model
.as_ref()?;
Some(SelectedModel {
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
model: LanguageModelId::from(selection.model.clone()),
})
}
/// Translate a stored model selection into the configured model from the registry.
fn resolve_model_from_selection(
selection: &LanguageModelSelection,
@ -3394,7 +3414,9 @@ impl Thread {
let mut title = SharedString::from(&tool_use.name);
let mut kind = acp::ToolKind::Other;
if let Some(tool) = tool.as_ref() {
title = tool.initial_title(tool_use.input.clone(), cx);
if let Ok(input) = tool_use.input.clone().into_json() {
title = tool.initial_title(input, cx);
}
kind = tool.kind();
}
@ -3411,16 +3433,33 @@ impl Thread {
}));
};
// Agent tools are JSON-schema tools. Custom text-tool deltas are rejected
// before considering partial-vs-complete input for these local tools.
let input = match tool_use.input.clone().into_json() {
Ok(input) => input,
Err(error) => {
return Some(Task::ready(LanguageModelToolResult {
content: vec![LanguageModelToolResultContent::Text(Arc::from(
error.to_string(),
))],
tool_use_id: tool_use.id,
tool_name: tool_use.name,
is_error: true,
output: None,
}));
}
};
if !tool_use.is_input_complete {
if tool.supports_input_streaming() {
let running_turn = self.running_turn.as_mut()?;
if let Some(sender) = running_turn.streaming_tool_inputs.get_mut(&tool_use.id) {
sender.send_partial(tool_use.input);
sender.send_partial(input);
return None;
}
let (mut sender, tool_input) = ToolInputSender::channel();
sender.send_partial(tool_use.input);
sender.send_partial(input);
running_turn
.streaming_tool_inputs
.insert(tool_use.id.clone(), sender);
@ -3447,12 +3486,12 @@ impl Thread {
.streaming_tool_inputs
.remove(&tool_use.id)
{
sender.send_full(tool_use.input);
sender.send_full(input);
return None;
}
log::debug!("Running tool {}", tool_use.name);
let tool_input = ToolInput::ready(tool_use.input);
let tool_input = ToolInput::ready(input);
Some(self.run_tool(
tool,
tool_input,
@ -3576,7 +3615,7 @@ impl Thread {
id: tool_use_id,
name: tool_name,
raw_input: raw_input.to_string(),
input: serde_json::json!({}),
input: language_model::LanguageModelToolUseInput::Json(serde_json::json!({})),
is_input_complete: true,
thought_signature: None,
};
@ -3652,7 +3691,7 @@ impl Thread {
&tool_use.name,
title,
kind,
tool_use.input.clone(),
tool_use.input.to_display_json(),
);
last_message
.content
@ -3663,7 +3702,7 @@ impl Thread {
acp::ToolCallUpdateFields::new()
.title(title.as_str())
.kind(kind)
.raw_input(tool_use.input.clone()),
.raw_input(tool_use.input.to_display_json()),
None,
);
}
@ -3903,12 +3942,12 @@ impl Thread {
.iter()
.filter_map(|(tool_name, tool)| {
log::trace!("Including tool: {}", tool_name);
Some(LanguageModelRequestTool {
name: tool_name.to_string(),
description: tool.description().to_string(),
input_schema: tool.input_schema(model.tool_input_format()).log_err()?,
use_input_streaming: tool.supports_input_streaming(),
})
Some(LanguageModelRequestTool::function(
tool_name.to_string(),
tool.description().to_string(),
tool.input_schema(model.tool_input_format()).log_err()?,
tool.supports_input_streaming(),
))
})
.collect::<Vec<_>>()
} else {
@ -4767,6 +4806,10 @@ pub struct TitleUpdated;
impl EventEmitter<TitleUpdated> for Thread {}
pub struct ModelChanged;
impl EventEmitter<ModelChanged> for Thread {}
/// A channel-based wrapper that delivers tool input to a running tool.
///
/// For non-streaming tools, created via `ToolInput::ready()` so `.recv()` resolves immediately.
@ -5883,10 +5926,15 @@ impl ToolCallEventStream {
&self,
command: Option<String>,
reason: String,
docs_section: Option<String>,
retries: usize,
cx: &mut App,
) -> Task<Result<SandboxFallbackDecision>> {
let details = acp_thread::SandboxFallbackAuthorizationDetails { command, reason };
let details = acp_thread::SandboxFallbackAuthorizationDetails {
command,
reason,
docs_section,
};
let retry_label = if retries == 0 {
"Retry".to_string()
} else {
@ -7643,6 +7691,7 @@ mod tests {
event_stream.authorize_sandbox_fallback(
Some("cargo build".to_string()),
"bwrap not found on PATH".to_string(),
Some("installing-bubblewrap".to_string()),
0,
cx,
)
@ -7654,6 +7703,10 @@ mod tests {
.expect("fallback authorization should include details");
assert_eq!(details.command.as_deref(), Some("cargo build"));
assert_eq!(details.reason, "bwrap not found on PATH");
assert_eq!(
details.docs_section.as_deref(),
Some("installing-bubblewrap")
);
let acp_thread::PermissionOptions::Flat(options) = &authorization.options else {
panic!("expected flat fallback permission options");
@ -7694,6 +7747,7 @@ mod tests {
event_stream.authorize_sandbox_fallback(
None,
"probe failed".to_string(),
None,
retries,
cx,
)
@ -7738,6 +7792,7 @@ mod tests {
event_stream.authorize_sandbox_fallback(
Some("cargo build".to_string()),
"user namespaces are disabled".to_string(),
None,
0,
cx,
)
@ -7767,7 +7822,13 @@ mod tests {
let (event_stream, mut receiver) = ToolCallEventStream::test();
let authorize = cx.update(|cx| {
event_stream.authorize_sandbox_fallback(None, "bwrap probe failed".to_string(), 0, cx)
event_stream.authorize_sandbox_fallback(
None,
"bwrap probe failed".to_string(),
None,
0,
cx,
)
});
let authorization = receiver.expect_authorization().await;
authorization
@ -7842,7 +7903,7 @@ mod tests {
id: registered_tool_use_id.clone(),
name: ReplayImageTool::NAME.into(),
raw_input: "null".to_string(),
input: json!(null),
input: language_model::LanguageModelToolUseInput::Json(json!(null)),
is_input_complete: true,
thought_signature: None,
};
@ -7850,7 +7911,7 @@ mod tests {
id: missing_tool_use_id.clone(),
name: "missing_image_tool".into(),
raw_input: "{}".to_string(),
input: json!({}),
input: language_model::LanguageModelToolUseInput::Json(json!({})),
is_input_complete: true,
thought_signature: None,
};
@ -8157,7 +8218,10 @@ mod tests {
assert_eq!(tool_use.raw_input, raw_input.to_string());
assert!(tool_use.is_input_complete);
// Should fall back to empty object for invalid JSON
assert_eq!(tool_use.input, json!({}));
assert_eq!(
tool_use.input,
language_model::LanguageModelToolUseInput::Json(json!({}))
);
}
_ => panic!("Expected ToolUse content"),
}

View file

@ -153,12 +153,12 @@ macro_rules! tools {
/// A list of all built-in tools
pub fn built_in_tools() -> impl Iterator<Item = LanguageModelRequestTool> {
fn language_model_tool<T: AgentTool>() -> LanguageModelRequestTool {
LanguageModelRequestTool {
name: T::NAME.to_string(),
description: T::description().to_string(),
input_schema: T::input_schema(LanguageModelToolSchemaFormat::JsonSchema).to_value(),
use_input_streaming: T::supports_input_streaming(),
}
LanguageModelRequestTool::function(
T::NAME.to_string(),
T::description().to_string(),
T::input_schema(LanguageModelToolSchemaFormat::JsonSchema).to_value(),
T::supports_input_streaming(),
)
}
[
$(

View file

@ -5,7 +5,7 @@ use super::tool_permissions::{
use agent_client_protocol::schema::v1 as acp;
use agent_settings::AgentSettings;
use futures::FutureExt as _;
use gpui::{App, Entity, SharedString, Task};
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -17,12 +17,13 @@ use crate::{
AgentTool, ToolCallEventStream, ToolInput, ToolPermissionDecision,
authorize_with_sensitive_settings, decide_permission_for_path,
};
use std::path::Path;
use std::path::{Path, PathBuf};
/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
/// Creates a new directory at the specified path, and all necessary parent directories. Returns confirmation that the directory was created.
///
/// This tool creates a directory and all necessary parent directories. It should be used whenever you need to create new directories within the project.
/// The only supported path outside the project is `~/.agents/skills` or a descendant, for global agent skills.
/// Use this whenever you need to create new directories. Paths inside the project are created directly.
///
/// This tool can also create a directory **outside** the project. When agent terminal commands are sandboxed, doing so grants those commands write access to exactly that new directory — so, rather than requesting write access to a broad existing parent (e.g. your home directory) just to create something inside it, create the specific directory here first and then write into it. The only other supported path outside the project is `~/.agents/skills` or a descendant, for global agent skills.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateDirectoryToolInput {
/// The path of the new directory.
@ -40,6 +41,13 @@ pub struct CreateDirectoryToolInput {
/// To create a global agent skill directory, you may provide a path under `~/.agents/skills`, such as `~/.agents/skills/my-skill`.
/// </example>
pub path: String,
/// Justification for creating a directory **outside** the project, shown to
/// the user (attributed to you) in the approval prompt that grants sandboxed
/// terminal commands write access to it. Required only for out-of-project
/// paths; ignored for paths inside the project or the global skills dir.
#[serde(default)]
pub reason: Option<String>,
}
pub struct CreateDirectoryTool {
@ -83,6 +91,28 @@ impl AgentTool for CreateDirectoryTool {
let project = self.project.clone();
cx.spawn(async move |cx| {
let input = input.recv().await.map_err(|e| e.to_string())?;
let fs = project.read_with(cx, |project, _cx| project.fs().clone());
// Resolve where this directory lives. The global agent-skills dir is a
// special case allowed outside the project; anything else outside the
// project is handled as a narrow sandbox write grant below.
let global_skill_directory =
resolve_creatable_global_skill_path(Path::new(&input.path), fs.as_ref()).await;
let in_project = project.read_with(cx, |project, cx| {
project.find_project_path(&input.path, cx).is_some()
});
// A path outside the project (and not the global skills dir) can only
// be created as a narrow sandbox write grant: create the directory and
// grant sandboxed terminal commands write access to exactly it. The
// sandbox approval prompt — which shows the real, canonicalized target
// — fully replaces the normal permission and symlink-escape prompts
// here.
if global_skill_directory.is_none() && !in_project {
return create_out_of_project_directory(&project, &input, &event_stream, cx).await;
}
let decision = cx.update(|cx| {
decide_permission_for_path(Self::NAME, &input.path, AgentSettings::get_global(cx))
});
@ -93,7 +123,6 @@ impl AgentTool for CreateDirectoryTool {
let destination_path: Arc<str> = input.path.as_str().into();
let fs = project.read_with(cx, |project, _cx| project.fs().clone());
let canonical_roots = canonicalize_worktree_roots(&project, &fs, cx).await;
let symlink_escape_target = project.read_with(cx, |project, cx| {
@ -149,9 +178,7 @@ impl AgentTool for CreateDirectoryTool {
authorize.await.map_err(|e| e.to_string())?;
}
if let Some(global_skill_directory) =
resolve_creatable_global_skill_path(Path::new(&input.path), fs.as_ref()).await
{
if let Some(global_skill_directory) = global_skill_directory {
futures::select! {
result = fs.create_dir(&global_skill_directory).fuse() => {
result.map_err(|e| format!("Creating directory {destination_path}: {e}"))?;
@ -185,6 +212,100 @@ impl AgentTool for CreateDirectoryTool {
}
}
/// Create a directory that lives **outside** the project by granting sandboxed
/// terminal commands write access to exactly it.
///
/// The directory is created (Linux: eagerly, pinning the inode; macOS: after
/// approval) and the user is shown the real, canonicalized target in the sandbox
/// approval prompt — which is what defends against a concurrent symlink swap: the
/// grant is always against the inode/path the user actually saw. On denial, only
/// the directories we created are removed.
async fn create_out_of_project_directory(
project: &Entity<Project>,
input: &CreateDirectoryToolInput,
event_stream: &ToolCallEventStream,
cx: &mut AsyncApp,
) -> Result<String, String> {
// Narrowing a grant to a brand-new directory only makes sense when the
// project's terminal commands are sandboxed, and only on platforms that can
// grant a not-yet-existing directory. Otherwise keep the historical
// "outside the project" rejection.
let sandboxing = project.read_with(cx, |project, cx| {
crate::sandboxing::sandboxing_enabled_for_project(project, cx)
});
let platform_supported = cfg!(any(target_os = "linux", target_os = "macos"));
if !sandboxing || !platform_supported {
return Err("Path to create was outside the project".to_string());
}
let Some(reason) = input
.reason
.as_deref()
.map(str::trim)
.filter(|reason| !reason.is_empty())
else {
return Err(
"Creating a directory outside the project grants sandboxed terminal commands write \
access to it, so a `reason` is required: briefly justify why the directory is needed, \
then try again."
.to_string(),
);
};
let reason = reason.to_string();
let absolute = resolve_absolute_path(project, &input.path, cx)
.ok_or_else(|| format!("Couldn't resolve `{}` to an absolute path.", input.path))?;
let prepared = cx
.background_spawn(async move { sandbox::GrantableWriteDir::prepare(&absolute) })
.await
.map_err(|error| format!("Creating directory {}: {error}", input.path))?;
let canonical = prepared.canonical_path().to_path_buf();
let request = crate::sandboxing::SandboxRequest {
write_paths: vec![canonical.clone()],
..Default::default()
};
let approve = cx.update(|cx| event_stream.authorize_sandbox(request, reason, cx));
match approve.await {
Ok(()) => {
let display = canonical.display().to_string();
cx.background_spawn(async move { prepared.finalize() })
.await
.map_err(|error| format!("Creating directory {display}: {error}"))?;
Ok(format!("Created directory {display}"))
}
Err(error) => {
// Roll back exactly what we created; leave the user no litter.
cx.background_spawn(async move { prepared.discard() }).await;
Err(format!("Create directory cancelled: {error}"))
}
}
}
/// Resolve a model-provided path to an absolute, lexically-normalized path.
/// Relative paths are joined onto the first worktree root.
fn resolve_absolute_path(
project: &Entity<Project>,
raw: &str,
cx: &mut AsyncApp,
) -> Option<PathBuf> {
let path = Path::new(raw);
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
let base = project.read_with(cx, |project, cx| {
project
.worktrees(cx)
.next()
.map(|worktree| worktree.read(cx).abs_path().to_path_buf())
})?;
base.join(path)
};
util::paths::normalize_lexically(&absolute).ok()
}
#[cfg(test)]
mod tests {
use super::*;
@ -231,7 +352,10 @@ mod tests {
let (event_stream, mut event_rx) = ToolCallEventStream::test();
let task = cx.update(|cx| {
tool.run(
ToolInput::resolved(CreateDirectoryToolInput { path: input_path }),
ToolInput::resolved(CreateDirectoryToolInput {
path: input_path,
reason: None,
}),
event_stream,
cx,
)
@ -286,6 +410,7 @@ mod tests {
tool.run(
ToolInput::resolved(CreateDirectoryToolInput {
path: outside_path.to_string_lossy().into_owned(),
reason: None,
}),
event_stream,
cx,
@ -342,6 +467,7 @@ mod tests {
tool.run(
ToolInput::resolved(CreateDirectoryToolInput {
path: "project/link_to_external".into(),
reason: None,
}),
event_stream,
cx,
@ -404,6 +530,7 @@ mod tests {
tool.run(
ToolInput::resolved(CreateDirectoryToolInput {
path: "project/link_to_external".into(),
reason: None,
}),
event_stream,
cx,
@ -463,6 +590,7 @@ mod tests {
tool.run(
ToolInput::resolved(CreateDirectoryToolInput {
path: "project/link_to_external".into(),
reason: None,
}),
event_stream,
cx,
@ -545,6 +673,7 @@ mod tests {
tool.run(
ToolInput::resolved(CreateDirectoryToolInput {
path: "project/link_to_external".into(),
reason: None,
}),
event_stream,
cx,
@ -561,4 +690,107 @@ mod tests {
"Deny policy should not emit symlink authorization prompt",
);
}
/// Out-of-project creation goes through the sandbox write-grant prompt and,
/// on approval, creates the *specific* new directory (not its broad parent).
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[gpui::test]
async fn test_create_directory_out_of_project_creates_and_grants(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/root"), json!({ "project": { "src": {} } }))
.await;
let project = Project::test(fs.clone(), [path!("/root/project").as_ref()], cx).await;
cx.executor().run_until_parked();
// The sandbox create path operates on the *real* filesystem, so use a
// real directory outside the (fake) project.
let scratch = tempfile::tempdir().unwrap();
let target = scratch.path().join("new_grant_dir");
assert!(!target.exists());
let tool = Arc::new(CreateDirectoryTool::new(project));
let (event_stream, mut event_rx) = ToolCallEventStream::test();
let path_input = target.to_string_lossy().into_owned();
let task = cx.update(|cx| {
tool.run(
ToolInput::resolved(CreateDirectoryToolInput {
path: path_input,
reason: Some("scratch space for the build".into()),
}),
event_stream,
cx,
)
});
let auth = event_rx.expect_authorization().await;
let details = acp_thread::sandbox_authorization_details_from_meta(&auth.tool_call.meta)
.expect("out-of-project create should request a sandbox write grant");
// The grant is for exactly the new directory, not its parent.
assert_eq!(
details.write_paths,
vec![scratch.path().canonicalize().unwrap().join("new_grant_dir")]
);
auth.response
.send(acp_thread::SelectedPermissionOutcome::new(
acp::PermissionOptionId::new(acp_thread::SandboxPermission::AllowThread.as_id()),
acp::PermissionOptionKind::AllowAlways,
))
.unwrap();
let result = task.await;
assert!(result.is_ok(), "expected success, got {result:?}");
assert!(
target.is_dir(),
"the new directory should have been created"
);
}
/// Denying the grant removes the directory we eagerly created, leaving no
/// trace on the filesystem.
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[gpui::test]
async fn test_create_directory_out_of_project_denied_cleans_up(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/root"), json!({ "project": { "src": {} } }))
.await;
let project = Project::test(fs.clone(), [path!("/root/project").as_ref()], cx).await;
cx.executor().run_until_parked();
let scratch = tempfile::tempdir().unwrap();
let target = scratch.path().join("denied_dir");
let tool = Arc::new(CreateDirectoryTool::new(project));
let (event_stream, mut event_rx) = ToolCallEventStream::test();
let path_input = target.to_string_lossy().into_owned();
let task = cx.update(|cx| {
tool.run(
ToolInput::resolved(CreateDirectoryToolInput {
path: path_input,
reason: Some("scratch space".into()),
}),
event_stream,
cx,
)
});
let auth = event_rx.expect_authorization().await;
auth.response
.send(acp_thread::SelectedPermissionOutcome::new(
acp::PermissionOptionId::new(acp_thread::SandboxPermission::Deny.as_id()),
acp::PermissionOptionKind::RejectOnce,
))
.unwrap();
let result = task.await;
assert!(result.is_err(), "denied create should fail");
assert!(
!target.exists(),
"denied create should leave no directory behind"
);
}
}

View file

@ -324,6 +324,77 @@ mod tests {
assert_eq!(new_text, "line 1\nmodified line 2\nline 3\n");
}
#[gpui::test]
async fn test_streaming_edit_first_line_missing_indent(cx: &mut TestAppContext) {
// Reproduces https://github.com/zed-industries/zed/issues/60302: the
// first line of the multi-line `old_text` omits its leading
// indentation while subsequent lines include theirs, so the indent
// delta computed from the first line must not be applied to the
// following lines. `old_text` also omits the `self.extra` line, so
// the query lines don't correspond one-to-one to the matched buffer
// rows and the indent pairing must follow the fuzzy match's
// alignment instead of assuming equal line counts.
let content = concat!(
"class Outer:\n",
" def method(self):\n",
" self.kept = \"unchanged\"\n",
" self.target_a = \"before\"\n",
" self.extra = \"row\"\n",
" self.target_b = \"before\"\n",
" self.target_c = \"before\"\n",
" self.target_d = \"before\"\n",
" self.kept_2 = \"unchanged\"\n",
);
let (edit_tool, _project, _action_log, _fs, _thread) =
setup_test(cx, json!({"file.py": content})).await;
let result = cx
.update(|cx| {
edit_tool.clone().run(
ToolInput::resolved(EditFileToolInput {
path: "root/file.py".into(),
edits: vec![Edit {
old_text: concat!(
"self.target_a = \"before\"\n",
" self.target_b = \"before\"\n",
" self.target_c = \"before\"\n",
" self.target_d = \"before\"",
)
.into(),
new_text: concat!(
"self.target_a = \"after\"\n",
" self.target_b = \"after\"\n",
" self.target_c = \"after\"\n",
" self.target_d = \"after\"",
)
.into(),
}],
}),
ToolCallEventStream::test().0,
cx,
)
})
.await;
let EditFileToolOutput::Success { new_text, .. } = result.unwrap() else {
panic!("expected success");
};
// The matched range includes the `self.extra` row, so it is replaced
// along with the rest of the match.
assert_eq!(
new_text,
concat!(
"class Outer:\n",
" def method(self):\n",
" self.kept = \"unchanged\"\n",
" self.target_a = \"after\"\n",
" self.target_b = \"after\"\n",
" self.target_c = \"after\"\n",
" self.target_d = \"after\"\n",
" self.kept_2 = \"unchanged\"\n",
)
);
}
#[gpui::test]
async fn test_streaming_edit_multiple_edits(cx: &mut TestAppContext) {
let (edit_tool, _project, _action_log, _fs, _thread) = setup_test(

View file

@ -16,7 +16,7 @@ use language::{Buffer, BufferEditSource, BufferEvent, LanguageRegistry};
use language_model::LanguageModelToolResultContent;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
use project::{AgentLocation, Project, ProjectPath};
use reindent::{Reindenter, compute_indent_delta};
use reindent::{Reindenter, compute_indent_delta, compute_rest_indent_delta};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::ops::Range;
@ -527,15 +527,34 @@ impl EditPipeline {
);
let buffer_indent = snapshot.line_indent_for_row(line);
let query_lines = matcher.query_lines();
let query_indent = text::LineIndent::from_iter(
matcher
.query_lines()
query_lines
.first()
.map(|s| s.as_str())
.unwrap_or("")
.chars(),
);
let indent_delta = compute_indent_delta(buffer_indent, query_indent);
let first_line_delta = compute_indent_delta(buffer_indent, query_indent);
// Query row 0 is excluded: its delta is `first_line_delta`,
// which intentionally differs when the model stripped the
// first line's indentation.
let rest_delta = compute_rest_indent_delta(
first_line_delta,
matcher
.line_pairs(&range)
.unwrap_or(&[])
.iter()
.filter(|(query_row, _)| *query_row != 0)
.filter_map(|(query_row, buffer_row)| {
let query_line = query_lines.get(*query_row as usize)?;
Some((
snapshot.line_indent_for_row(*buffer_row),
text::LineIndent::from_iter(query_line.chars()),
))
}),
);
let old_text_in_buffer = snapshot.text_for_range(range.clone()).collect::<String>();
@ -551,7 +570,7 @@ impl EditPipeline {
self.current_edit = Some(EditPipelineEntry::StreamingNewText {
streaming_diff: StreamingDiff::new(old_text_in_buffer),
edit_cursor: range.start,
reindenter: Reindenter::new(indent_delta),
reindenter: Reindenter::with_deltas(first_line_delta, rest_delta),
original_snapshot: text_snapshot,
});

View file

@ -1,7 +1,7 @@
use language::LineIndent;
use std::{cmp, iter};
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum IndentDelta {
Spaces(isize),
Tabs(isize),
@ -31,20 +31,60 @@ pub fn compute_indent_delta(buffer_indent: LineIndent, query_indent: LineIndent)
}
}
/// Computes the indent delta for the lines after the first, given per-line
/// `(buffer, query)` indents for those lines.
///
/// When the remaining lines agree on a consistent delta, that delta is
/// returned even if it differs from `first_line_delta`. This handles queries
/// where only the first line's indentation was stripped. When the remaining
/// lines are inconsistent (or all blank), falls back to `first_line_delta`,
/// preserving the uniform re-indentation behavior.
pub fn compute_rest_indent_delta(
first_line_delta: IndentDelta,
indent_pairs: impl IntoIterator<Item = (LineIndent, LineIndent)>,
) -> IndentDelta {
let mut rest_delta = None;
for (buffer_indent, query_indent) in indent_pairs {
if buffer_indent.line_blank || query_indent.line_blank {
continue;
}
let delta = compute_indent_delta(buffer_indent, query_indent);
match rest_delta {
None => rest_delta = Some(delta),
Some(existing) if existing == delta => {}
Some(_) => return first_line_delta,
}
}
rest_delta.unwrap_or(first_line_delta)
}
/// Synchronous re-indentation adapter. Buffers incomplete lines and applies
/// an `IndentDelta` to each line's leading whitespace before emitting it.
///
/// Models sometimes omit the leading indentation only on the first line of
/// `old_text`/`new_text` (e.g. when copying from mid-line context), so the
/// first line and the remaining lines can require different deltas.
pub struct Reindenter {
delta: IndentDelta,
first_line_delta: IndentDelta,
rest_delta: IndentDelta,
buffer: String,
in_leading_whitespace: bool,
on_first_line: bool,
}
impl Reindenter {
pub fn new(delta: IndentDelta) -> Self {
#[cfg(test)]
fn uniform(delta: IndentDelta) -> Self {
Self::with_deltas(delta, delta)
}
pub fn with_deltas(first_line_delta: IndentDelta, rest_delta: IndentDelta) -> Self {
Self {
delta,
first_line_delta,
rest_delta,
buffer: String::new(),
in_leading_whitespace: true,
on_first_line: true,
}
}
@ -70,14 +110,19 @@ impl Reindenter {
None => (self.buffer.len(), true),
};
let line = &self.buffer[start_ix..line_end];
let delta = if self.on_first_line {
self.first_line_delta
} else {
self.rest_delta
};
if self.in_leading_whitespace {
if let Some(non_whitespace_ix) = line.find(|c| self.delta.character() != c) {
if let Some(non_whitespace_ix) = line.find(|c| delta.character() != c) {
// We found a non-whitespace character, adjust indentation
// based on the delta.
let new_indent_len =
cmp::max(0, non_whitespace_ix as isize + self.delta.len()) as usize;
indented.extend(iter::repeat(self.delta.character()).take(new_indent_len));
cmp::max(0, non_whitespace_ix as isize + delta.len()) as usize;
indented.extend(iter::repeat(delta.character()).take(new_indent_len));
indented.push_str(&line[non_whitespace_ix..]);
self.in_leading_whitespace = false;
} else if is_pending_line && !is_final {
@ -97,6 +142,7 @@ impl Reindenter {
break;
} else {
self.in_leading_whitespace = true;
self.on_first_line = false;
indented.push('\n');
start_ix = line_end + 1;
}
@ -116,7 +162,7 @@ mod tests {
#[test]
fn test_indent_single_chunk() {
let mut r = Reindenter::new(IndentDelta::Spaces(2));
let mut r = Reindenter::uniform(IndentDelta::Spaces(2));
let out = r.push(" abc\n def\n ghi");
// All three lines are emitted: "ghi" starts with spaces but
// contains non-whitespace, so it's processed immediately.
@ -127,7 +173,7 @@ mod tests {
#[test]
fn test_outdent_tabs() {
let mut r = Reindenter::new(IndentDelta::Tabs(-2));
let mut r = Reindenter::uniform(IndentDelta::Tabs(-2));
let out = r.push("\t\t\t\tabc\n\t\tdef\n\t\t\t\t\t\tghi");
assert_eq!(out, "\t\tabc\ndef\n\t\t\t\tghi");
let out = r.finish();
@ -136,7 +182,7 @@ mod tests {
#[test]
fn test_incremental_chunks() {
let mut r = Reindenter::new(IndentDelta::Spaces(2));
let mut r = Reindenter::uniform(IndentDelta::Spaces(2));
// Feed " ab" — the `a` is non-whitespace, so the line is
// processed immediately even without a trailing newline.
let out = r.push(" ab");
@ -151,7 +197,7 @@ mod tests {
#[test]
fn test_zero_delta() {
let mut r = Reindenter::new(IndentDelta::Spaces(0));
let mut r = Reindenter::uniform(IndentDelta::Spaces(0));
let out = r.push(" hello\n world\n");
assert_eq!(out, " hello\n world\n");
let out = r.finish();
@ -160,7 +206,7 @@ mod tests {
#[test]
fn test_clamp_negative_indent() {
let mut r = Reindenter::new(IndentDelta::Spaces(-10));
let mut r = Reindenter::uniform(IndentDelta::Spaces(-10));
let out = r.push(" abc\n");
// max(0, 2 - 10) = 0, so no leading spaces.
assert_eq!(out, "abc\n");
@ -170,7 +216,7 @@ mod tests {
#[test]
fn test_whitespace_only_lines() {
let mut r = Reindenter::new(IndentDelta::Spaces(2));
let mut r = Reindenter::uniform(IndentDelta::Spaces(2));
let out = r.push(" \n code\n");
// First line is all whitespace — emitted verbatim. Second line is indented.
assert_eq!(out, " \n code\n");
@ -178,6 +224,95 @@ mod tests {
assert_eq!(out, "");
}
#[test]
fn test_distinct_first_line_delta() {
// First line's indentation was stripped in the query (delta +8),
// while the remaining lines are already correct (delta 0). Chunks
// split mid-line and mid-indentation to exercise the streaming path,
// and the blank line is passed through verbatim.
let mut r = Reindenter::with_deltas(IndentDelta::Spaces(8), IndentDelta::Spaces(0));
let mut out = r.push("self.target_a = ");
out.push_str(&r.push("\"after\"\n "));
out.push_str(&r.push(" self.target_b = \"after\"\n"));
out.push_str(&r.push("\n self.target_c = \"after\""));
out.push_str(&r.finish());
assert_eq!(
out,
concat!(
" self.target_a = \"after\"\n",
" self.target_b = \"after\"\n",
"\n",
" self.target_c = \"after\"",
)
);
}
fn line_indent(text: &str) -> LineIndent {
LineIndent::from_iter(text.chars())
}
#[test]
fn test_compute_rest_indent_delta() {
let first_line_delta = IndentDelta::Spaces(8);
// Remaining lines that agree on a delta override the first-line
// delta, and blank lines are skipped when forming the consensus.
assert_eq!(
compute_rest_indent_delta(
first_line_delta,
vec![
(line_indent(" b"), line_indent(" b")),
(line_indent(""), line_indent("")),
(line_indent(" c"), line_indent(" c")),
],
),
IndentDelta::Spaces(0)
);
assert_eq!(
compute_rest_indent_delta(
first_line_delta,
vec![
(line_indent(" b"), line_indent(" b")),
(line_indent(" "), line_indent("")),
(line_indent(" c"), line_indent(" c")),
],
),
IndentDelta::Spaces(4)
);
assert_eq!(
compute_rest_indent_delta(
first_line_delta,
vec![(line_indent("\t\tb"), line_indent("\tb"))],
),
IndentDelta::Tabs(1)
);
// Inconsistent remaining lines fall back to the first-line delta...
assert_eq!(
compute_rest_indent_delta(
first_line_delta,
vec![
(line_indent(" b"), line_indent(" b")),
(line_indent(" c"), line_indent(" c")),
],
),
first_line_delta
);
// ...and so do all-blank and empty pairings.
assert_eq!(
compute_rest_indent_delta(
first_line_delta,
vec![(line_indent(" "), line_indent(""))],
),
first_line_delta
);
assert_eq!(
compute_rest_indent_delta(first_line_delta, vec![]),
first_line_delta
);
}
#[test]
fn test_compute_indent_delta_spaces() {
let buffer = LineIndent {

View file

@ -12,10 +12,18 @@ pub struct StreamingFuzzyMatcher {
query_lines: Vec<String>,
line_hint: Option<u32>,
incomplete_line: String,
matches: Vec<Range<usize>>,
matches: Vec<SearchMatch>,
matrix: SearchMatrix,
}
/// A match candidate: the matched byte range plus the 0-based
/// `(query_row, buffer_row)` line pairs the search aligned to produce it.
#[derive(Clone, Debug)]
struct SearchMatch {
range: Range<usize>,
line_pairs: Vec<(u32, u32)>,
}
impl StreamingFuzzyMatcher {
pub fn new(snapshot: TextBufferSnapshot) -> Self {
let buffer_line_count = snapshot.max_point().row as usize + 1;
@ -34,6 +42,23 @@ impl StreamingFuzzyMatcher {
&self.query_lines
}
/// Returns the 0-based `(query_row, buffer_row)` line pairs that the
/// search aligned for the match with the given range. Lines that were
/// skipped on either side of the alignment are absent.
pub fn line_pairs(&self, range: &Range<usize>) -> Option<&[(u32, u32)]> {
self.matches
.iter()
.find(|search_match| search_match.range == *range)
.map(|search_match| search_match.line_pairs.as_slice())
}
fn match_ranges(&self) -> Vec<Range<usize>> {
self.matches
.iter()
.map(|search_match| search_match.range.clone())
.collect()
}
/// Push a new chunk of text and get the best match found so far.
///
/// This method accumulates text chunks and processes complete lines.
@ -62,7 +87,11 @@ impl StreamingFuzzyMatcher {
}
let best_match = self.select_best_match();
best_match.or_else(|| self.matches.first().cloned())
best_match.or_else(|| {
self.matches
.first()
.map(|search_match| search_match.range.clone())
})
}
/// Finish processing and return the final best match(es).
@ -72,26 +101,35 @@ impl StreamingFuzzyMatcher {
pub fn finish(&mut self) -> Vec<Range<usize>> {
// Process any remaining incomplete line
if !self.incomplete_line.is_empty() {
if self.matches.len() == 1 {
let range = &mut self.matches[0];
if let [only_match] = self.matches.as_mut_slice() {
let range = &mut only_match.range;
if range.end < self.snapshot.len()
&& self
.snapshot
.contains_str_at(range.end + 1, &self.incomplete_line)
{
range.end += 1 + self.incomplete_line.len();
return self.matches.clone();
// Record the line and its alignment so that `query_lines`
// and `line_pairs` stay in sync with the lines covered by
// the returned range.
let extended_row = self.snapshot.offset_to_point(range.end).row;
self.query_lines
.push(std::mem::take(&mut self.incomplete_line));
only_match
.line_pairs
.push(((self.query_lines.len() - 1) as u32, extended_row));
return self.match_ranges();
}
}
self.query_lines.push(self.incomplete_line.clone());
self.incomplete_line.clear();
self.query_lines
.push(std::mem::take(&mut self.incomplete_line));
self.matches = self.resolve_location_fuzzy();
}
self.matches.clone()
self.match_ranges()
}
fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> {
fn resolve_location_fuzzy(&mut self) -> Vec<SearchMatch> {
let new_query_line_count = self.query_lines.len();
let old_query_line_count = self.matrix.rows.saturating_sub(1);
if new_query_line_count == old_query_line_count {
@ -167,7 +205,7 @@ impl StreamingFuzzyMatcher {
// Find ranges for the matches
let mut valid_matches = Vec::new();
for &buffer_row_end in &matches_with_best_cost {
let mut matched_lines = 0;
let mut line_pairs = Vec::new();
let mut query_row = new_query_line_count;
let mut buffer_row_start = buffer_row_end;
while query_row > 0 && buffer_row_start > 0 {
@ -176,7 +214,7 @@ impl StreamingFuzzyMatcher {
SearchDirection::Diagonal => {
query_row -= 1;
buffer_row_start -= 1;
matched_lines += 1;
line_pairs.push((query_row as u32, buffer_row_start));
}
SearchDirection::Up => {
query_row -= 1;
@ -186,9 +224,10 @@ impl StreamingFuzzyMatcher {
}
}
}
line_pairs.reverse();
let matched_buffer_row_count = buffer_row_end - buffer_row_start;
let matched_ratio = matched_lines as f32
let matched_ratio = line_pairs.len() as f32
/ (matched_buffer_row_count as f32).max(new_query_line_count as f32);
if matched_ratio >= 0.8 {
let buffer_start_ix = self
@ -198,11 +237,14 @@ impl StreamingFuzzyMatcher {
buffer_row_end - 1,
self.snapshot.line_len(buffer_row_end - 1),
));
valid_matches.push((buffer_row_start, buffer_start_ix..buffer_end_ix));
valid_matches.push(SearchMatch {
range: buffer_start_ix..buffer_end_ix,
line_pairs,
});
}
}
valid_matches.into_iter().map(|(_, range)| range).collect()
valid_matches
}
/// Return the best match with starting position close enough to line_hint.
@ -216,8 +258,8 @@ impl StreamingFuzzyMatcher {
return None;
}
if self.matches.len() == 1 {
return self.matches.first().cloned();
if let [only_match] = self.matches.as_slice() {
return Some(only_match.range.clone());
}
let Some(line_hint) = self.line_hint else {
@ -228,14 +270,14 @@ impl StreamingFuzzyMatcher {
let mut best_match = None;
let mut best_distance = u32::MAX;
for range in &self.matches {
let start_point = self.snapshot.offset_to_point(range.start);
for search_match in &self.matches {
let start_point = self.snapshot.offset_to_point(search_match.range.start);
let start_line = start_point.row;
let distance = start_line.abs_diff(line_hint);
if distance <= LINE_HINT_TOLERANCE && distance < best_distance {
best_distance = distance;
best_match = Some(range.clone());
best_match = Some(search_match.range.clone());
}
}
@ -831,6 +873,79 @@ mod tests {
}
}
#[test]
fn test_line_pairs_skip_unmatched_buffer_line() {
let text = indoc! {r#"
class Outer:
def method(self):
self.kept = "unchanged"
self.target_a = "before"
self.extra = "row"
self.target_b = "before"
self.target_c = "before"
self.target_d = "before"
self.kept_2 = "unchanged"
"#};
let buffer = TextBuffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
text.to_string(),
);
let mut matcher = StreamingFuzzyMatcher::new(buffer.snapshot().clone());
// The query omits the `self.extra` row that sits between the matched
// buffer lines.
matcher.push(
concat!(
" self.target_a = \"before\"\n",
" self.target_b = \"before\"\n",
" self.target_c = \"before\"\n",
" self.target_d = \"before\"\n",
),
None,
);
let matches = matcher.finish();
assert_eq!(matches.len(), 1);
assert_eq!(
matcher.line_pairs(&matches[0]),
Some(&[(0, 3), (1, 5), (2, 6), (3, 7)][..])
);
}
#[test]
fn test_line_pairs_include_extended_incomplete_line() {
let text = indoc! {r#"
fn on_query_change(&mut self, cx: &mut Context<Self>) {
self.filter(cx);
}
fn render_search(&self, cx: &mut Context<Self>) -> Div {
div()
}
"#};
let buffer = TextBuffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
text.to_string(),
);
let mut matcher = StreamingFuzzyMatcher::new(buffer.snapshot().clone());
// The last query line is incomplete and gets appended to the match by
// `finish` via verbatim comparison rather than the fuzzy search.
matcher.push("}\n\n\n\nfn render_search", None);
let matches = matcher.finish();
assert_eq!(matches.len(), 1);
assert_eq!(
matcher.line_pairs(&matches[0]),
Some(&[(0, 2), (1, 3), (2, 4), (3, 5), (4, 6)][..])
);
assert_eq!(matcher.query_lines().len(), 5);
}
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);

View file

@ -503,7 +503,9 @@ impl EditToolTest {
if tool_use.is_input_complete
&& tool_use.name.as_ref() == EditFileTool::NAME =>
{
let input: EditFileToolInput = serde_json::from_value(tool_use.input)
let input: EditFileToolInput = tool_use
.input
.parse()
.context("Failed to parse tool input as EditFileToolInput")?;
return Ok(input);
}
@ -607,7 +609,9 @@ fn tool_use(
id: LanguageModelToolUseId::from(id.into()),
name: name.into(),
raw_input: serde_json::to_string_pretty(&input).unwrap(),
input: serde_json::to_value(input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(input).unwrap(),
),
is_input_complete: true,
thought_signature: None,
})

View file

@ -327,7 +327,9 @@ async fn extract_tool_use(
Ok(LanguageModelCompletionEvent::ToolUse(tool_use))
if tool_use.is_input_complete && tool_use.name.as_ref() == TerminalTool::NAME =>
{
let input: TerminalToolInput = serde_json::from_value(tool_use.input)
let input: TerminalToolInput = tool_use
.input
.parse()
.context("Failed to parse tool input as TerminalToolInput")?;
return Ok(input);
}

View file

@ -323,7 +323,9 @@ impl WriteToolTest {
if tool_use.is_input_complete
&& tool_use.name.as_ref() == WriteFileTool::NAME =>
{
let input: WriteFileToolInput = serde_json::from_value(tool_use.input)
let input: WriteFileToolInput = tool_use
.input
.parse()
.context("Failed to parse tool input as WriteFileToolInput")?;
return Ok(input);
}
@ -410,7 +412,9 @@ fn tool_use(
id: LanguageModelToolUseId::from(id.into()),
name: name.into(),
raw_input: serde_json::to_string_pretty(&input).unwrap(),
input: serde_json::to_value(input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(input).unwrap(),
),
is_input_complete: true,
thought_signature: None,
})

View file

@ -23,12 +23,38 @@ enum ContentType {
Json,
}
/// The maximum number of HTTP redirects the fetch tool will follow. Each hop is
/// re-authorized against the shared network grants before being followed.
const MAX_REDIRECTS: usize = 20;
/// The outcome of a single (non-redirect-following) HTTP request.
enum FetchStep {
/// The server responded with a redirect to this absolute URL. Its host must
/// be authorized before the redirect is followed.
Redirect(String),
/// A terminal response was received and converted to Markdown.
Complete(String),
}
/// Prepends `https://` when the URL has no explicit HTTP(S) scheme, matching the
/// behavior the fetch tool has always had for user/model-supplied URLs.
fn normalize_url(url: &str) -> Cow<'_, str> {
if !url.starts_with("https://") && !url.starts_with("http://") {
Cow::Owned(format!("https://{url}"))
} else {
Cow::Borrowed(url)
}
}
/// Fetches a URL and returns the content as Markdown.
///
/// This tool is not run inside the terminal OS sandbox, but it still refuses to
/// reach any host that hasn't been granted network access. It shares the same
/// per-host grants as the `terminal` tool: approving a host for one authorizes
/// it for the other, whether the grant is for this thread or saved permanently.
/// HTTP redirects are followed one hop at a time, and each hop's host must be
/// granted the same way, so a granted host can't redirect the request to a host
/// that hasn't been approved.
/// When unsandboxed access has been granted, these restrictions are lifted
/// entirely, matching the terminal, which is also how loopback and IP-literal
/// hosts (which can't be granted individually) become reachable.
@ -47,14 +73,35 @@ impl FetchTool {
Self { http_client }
}
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
Cow::Owned(format!("https://{url}"))
} else {
Cow::Borrowed(url)
};
/// Performs a single HTTP GET *without* following redirects, so the tool can
/// re-authorize each hop against the shared network grants before following
/// it. Returns the redirect target when the server responds with a 3xx, or
/// the final content converted to Markdown otherwise.
async fn fetch_step(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<FetchStep> {
let normalized = normalize_url(url);
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut response = http_client
.get(&normalized, AsyncBody::default(), false)
.await?;
let status = response.status();
if status.is_redirection() {
let location = response
.headers()
.get("location")
.context("redirect response is missing a Location header")?
.to_str()
.context("redirect response has an invalid Location header")?;
let target = url::Url::parse(&normalized)
.with_context(|| format!("could not parse URL {normalized:?}"))?
.join(location)
.with_context(|| format!("invalid redirect target {location:?}"))?;
anyhow::ensure!(
matches!(target.scheme(), "http" | "https"),
"refusing to follow redirect to non-HTTP(S) URL {target}"
);
return Ok(FetchStep::Redirect(target.to_string()));
}
let mut body = Vec::new();
response
@ -63,12 +110,9 @@ impl FetchTool {
.await
.context("error reading response body")?;
if response.status().is_client_error() {
if status.is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
bail!("status error {}, response: {text:?}", status.as_u16());
}
let Some(content_type) = response.headers().get("content-type") else {
@ -86,7 +130,7 @@ impl FetchTool {
ContentType::Html
};
match content_type {
let text = match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
@ -96,7 +140,7 @@ impl FetchTool {
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
if normalized.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
@ -108,30 +152,25 @@ impl FetchTool {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
convert_html_to_markdown(&body[..], &mut handlers)?
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Plaintext => std::str::from_utf8(&body)?.to_owned(),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
format!("```json\n{}\n```", serde_json::to_string_pretty(&json)?)
}
}
};
Ok(FetchStep::Complete(text))
}
}
/// Extracts the host from a fetch URL as a [`http_proxy::HostPattern`] so it can
/// be matched against the shared network grants. Mirrors the scheme handling in
/// [`FetchTool::build_message`] (defaulting to `https://` when none is given).
/// [`normalize_url`] (defaulting to `https://` when none is given).
fn host_pattern_for_url(url: &str) -> Result<http_proxy::HostPattern> {
let normalized = if !url.starts_with("https://") && !url.starts_with("http://") {
Cow::Owned(format!("https://{url}"))
} else {
Cow::Borrowed(url)
};
let normalized = normalize_url(url);
let parsed =
url::Url::parse(&normalized).with_context(|| format!("could not parse URL {url:?}"))?;
let host = parsed
@ -211,36 +250,61 @@ impl AgentTool for FetchTool {
// already runs without isolation, so we drop fetch's restrictions
// too — including reaching hosts that can't be granted individually
// (loopback and IP literals).
//
// Crucially, this authorization is applied to every redirect hop as
// well as the initial URL, so a granted host can't 30x-redirect the
// fetch to a host the user never approved. We disable the HTTP
// client's own redirect following and re-run the grant for each hop
// before requesting it.
let unsandboxed = cx.update(|cx| event_stream.unsandboxed_access_granted(cx));
if !unsandboxed {
let host = host_pattern_for_url(&input.url).map_err(|e| e.to_string())?;
let authorize_host = cx.update(|cx| {
let request = SandboxRequest {
network: NetworkRequest::Hosts(vec![host]),
..Default::default()
let mut current_url = input.url.clone();
let mut redirects = 0;
let text = loop {
if !unsandboxed {
let host = host_pattern_for_url(&current_url).map_err(|e| e.to_string())?;
let authorize_host = cx.update(|cx| {
let request = SandboxRequest {
network: NetworkRequest::Hosts(vec![host]),
..Default::default()
};
event_stream.authorize_sandbox(request, String::new(), cx)
});
futures::select! {
result = authorize_host.fuse() => result.map_err(|e| e.to_string())?,
_ = event_stream.cancelled_by_user().fuse() => {
return Err("Fetch cancelled by user".to_string());
}
};
event_stream.authorize_sandbox(request, String::new(), cx)
}
let fetch_task = cx.background_spawn({
let http_client = http_client.clone();
let url = current_url.clone();
async move { Self::fetch_step(http_client, &url).await }
});
futures::select! {
result = authorize_host.fuse() => result.map_err(|e| e.to_string())?,
let step = futures::select! {
result = fetch_task.fuse() => result.map_err(|e| e.to_string())?,
_ = event_stream.cancelled_by_user().fuse() => {
return Err("Fetch cancelled by user".to_string());
}
};
}
let fetch_task = cx.background_spawn({
let http_client = http_client.clone();
let url = input.url.clone();
async move { Self::build_message(http_client, &url).await }
});
let text = futures::select! {
result = fetch_task.fuse() => result.map_err(|e| e.to_string())?,
_ = event_stream.cancelled_by_user().fuse() => {
return Err("Fetch cancelled by user".to_string());
match step {
FetchStep::Complete(text) => break text,
FetchStep::Redirect(target) => {
redirects += 1;
if redirects > MAX_REDIRECTS {
return Err(format!(
"exceeded the maximum of {MAX_REDIRECTS} redirects"
));
}
current_url = target;
}
}
};
if text.trim().is_empty() {
return Err("no textual content found".to_string());
}

View file

@ -1,4 +1,5 @@
use crate::{AgentTool, ToolCallEventStream, ToolInput};
use acp_thread::MentionUri;
use agent_client_protocol::schema::v1 as acp;
use anyhow::{Result, anyhow};
use futures::FutureExt as _;
@ -155,10 +156,13 @@ impl AgentTool for FindPathTool {
paginated_matches
.iter()
.map(|path| {
let uri = MentionUri::File {
abs_path: path.clone(),
};
acp::ToolCallContent::Content(acp::Content::new(
acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
path.to_string_lossy(),
format!("file://{}", path.display()),
uri.to_uri().to_string(),
)),
))
})

View file

@ -1,4 +1,5 @@
use crate::{AgentTool, ToolCallEventStream, ToolInput};
use acp_thread::MentionUri;
use agent_client_protocol::schema::v1 as acp;
use anyhow::Result;
use futures::{FutureExt as _, StreamExt};
@ -327,10 +328,15 @@ impl AgentTool for GrepTool {
output.push_str("\n```\n");
if let Some(abs_path) = &abs_path {
let uri = MentionUri::Selection {
abs_path: Some(abs_path.clone()),
line_range: range.start.row..=end_row,
column: None,
};
content.push(acp::ToolCallContent::Content(acp::Content::new(
acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
format!("{}#{}", path.display(), line_label),
format!("file://{}#{}", abs_path.display(), line_label),
uri.to_uri().to_string(),
)),
)));
locations.push(
@ -393,6 +399,7 @@ mod tests {
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
use std::path::PathBuf;
use unindent::Unindent;
use util::path;
@ -611,21 +618,30 @@ mod tests {
.collect::<Vec<_>>();
assert_eq!(links.len(), 2, "expected one resource link per match");
let alpha_uri = format!("file://{}#L1", path!("/root/src/alpha.txt"));
let selection_uri = |abs_path: &str| {
MentionUri::Selection {
abs_path: Some(PathBuf::from(abs_path)),
line_range: 0..=0,
column: None,
}
.to_uri()
.to_string()
};
let alpha_uri = selection_uri(path!("/root/src/alpha.txt"));
assert!(
links.iter().any(|link| {
link.name.replace('\\', "/") == "root/src/alpha.txt#L1"
&& link.uri.replace('\\', "/") == alpha_uri.replace('\\', "/")
link.name.replace('\\', "/") == "root/src/alpha.txt#L1" && link.uri == alpha_uri
}),
"missing clickable link for alpha.txt, got: {links:?}"
);
let beta_uri = format!("file://{}#L1", path!("/root/beta.txt"));
let beta_uri = selection_uri(path!("/root/beta.txt"));
assert!(
links.iter().any(|link| {
link.name.replace('\\', "/") == "root/beta.txt#L1"
&& link.uri.replace('\\', "/") == beta_uri.replace('\\', "/")
}),
links
.iter()
.any(|link| link.name.replace('\\', "/") == "root/beta.txt#L1"
&& link.uri == beta_uri),
"missing clickable link for beta.txt, got: {links:?}"
);

View file

@ -558,8 +558,10 @@ async fn run_terminal_tool(
if !path.is_dir() {
return Err(format!(
"Cannot request sandbox write access to `{}`: on Linux, write access can only \
be granted to directories that already exist. To create or modify files, \
request write access to the existing directory that contains them, not the \
be granted to directories that already exist. To create a new directory to write \
into, use the `create_directory` tool (which creates it and grants write access to \
exactly that directory) rather than requesting its parent. To modify existing \
files, request write access to the existing directory that contains them, not the \
file path itself.",
path.display()
));
@ -683,6 +685,7 @@ async fn run_terminal_tool(
event_stream.authorize_sandbox_fallback(
Some(input.command.clone()),
error.user_facing_message(),
Some(error.docs_section().to_string()),
retries,
cx,
)
@ -788,6 +791,7 @@ async fn run_terminal_tool(
event_stream.authorize_sandbox_fallback(
Some(input.command.clone()),
sandbox_error.user_facing_message(),
Some(sandbox_error.docs_section().to_string()),
retries,
cx,
)

View file

@ -753,10 +753,7 @@ fn connect_client_future(
)
}
fn client_capabilities_for_agent(
agent_id: &AgentId,
supports_beta_features: bool,
) -> acp::ClientCapabilities {
fn client_capabilities_for_agent(agent_id: &AgentId) -> acp::ClientCapabilities {
let mut meta = acp::Meta::from_iter([
("terminal_output".into(), true.into()),
("terminal-auth".into(), true.into()),
@ -766,30 +763,24 @@ fn client_capabilities_for_agent(
meta.insert(PARAMETERIZED_MODEL_PICKER_META_KEY.into(), true.into());
}
let mut capabilities = acp::ClientCapabilities::new()
acp::ClientCapabilities::new()
.fs(acp::FileSystemCapabilities::new()
.read_text_file(true)
.write_text_file(true))
.terminal(true)
.auth(acp::AuthCapabilities::new().terminal(true))
.meta(meta);
if supports_beta_features {
capabilities = capabilities
.elicitation(
acp::ElicitationCapabilities::new()
.form(acp::ElicitationFormCapabilities::new())
.url(acp::ElicitationUrlCapabilities::new()),
)
.session(
acp::ClientSessionCapabilities::new().config_options(
acp::SessionConfigOptionsCapabilities::new()
.boolean(acp::BooleanConfigOptionCapabilities::new()),
),
);
}
capabilities
.session(
acp::ClientSessionCapabilities::new().config_options(
acp::SessionConfigOptionsCapabilities::new()
.boolean(acp::BooleanConfigOptionCapabilities::new()),
),
)
.elicitation(
acp::ElicitationCapabilities::new()
.form(acp::ElicitationFormCapabilities::new())
.url(acp::ElicitationUrlCapabilities::new()),
)
.meta(meta)
}
impl AcpConnection {
@ -981,10 +972,7 @@ impl AcpConnection {
let initialize_response = connection
.send_request(
acp::InitializeRequest::new(ProtocolVersion::V1)
.client_capabilities(client_capabilities_for_agent(
&agent_id,
cx.update(|cx| cx.has_flag::<AcpBetaFeatureFlag>()),
))
.client_capabilities(client_capabilities_for_agent(&agent_id))
.client_info(
acp::Implementation::new("zed", version)
.title(release_channel.map(ToOwned::to_owned)),
@ -1300,7 +1288,6 @@ impl AcpConnection {
cx: &mut AsyncApp,
) {
let id = self.id.clone();
let apply_boolean_defaults = cx.update(|cx| cx.has_flag::<AcpBetaFeatureFlag>());
let defaults_to_apply: Vec<_> = {
let config_opts_ref = config_options.borrow();
config_opts_ref
@ -1333,9 +1320,6 @@ impl AcpConnection {
_ => None,
}
}
acp::SessionConfigKind::Boolean(_) if !apply_boolean_defaults => {
return None;
}
acp::SessionConfigKind::Boolean(_) => default_value
.as_bool()
.map(acp::SessionConfigOptionValue::boolean),
@ -2703,7 +2687,6 @@ mod tests {
use super::*;
use feature_flags::FeatureFlag as _;
use gpui::UpdateGlobal as _;
use settings::Settings as _;
fn init_feature_flags_test(cx: &mut gpui::TestAppContext) {
@ -2715,50 +2698,15 @@ mod tests {
});
}
fn set_acp_beta_override(value: &str, cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |content| {
content
.feature_flags
.get_or_insert_default()
.insert(AcpBetaFeatureFlag::NAME.to_string(), value.to_string());
});
});
});
}
#[gpui::test]
async fn client_capabilities_omit_elicitation_without_acp_beta(cx: &mut gpui::TestAppContext) {
async fn client_capabilities_include_elicitation_without_acp_beta(
cx: &mut gpui::TestAppContext,
) {
init_feature_flags_test(cx);
set_acp_beta_override("off", cx);
let capabilities = cx.update(|cx| {
client_capabilities_for_agent(
&AgentId::new("codex-acp"),
cx.has_flag::<AcpBetaFeatureFlag>(),
)
});
assert!(capabilities.elicitation.is_none());
}
#[gpui::test]
async fn client_capabilities_include_elicitation_with_acp_beta(cx: &mut gpui::TestAppContext) {
init_feature_flags_test(cx);
cx.update(|cx| {
cx.update_flags(false, vec![AcpBetaFeatureFlag::NAME.to_string()]);
});
let capabilities = cx.update(|cx| {
client_capabilities_for_agent(
&AgentId::new("codex-acp"),
cx.has_flag::<AcpBetaFeatureFlag>(),
)
});
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"));
let elicitation = capabilities
.elicitation
.expect("elicitation should be advertised when acp-beta is enabled");
.expect("elicitation should always be advertised");
assert!(elicitation.form.is_some());
assert!(elicitation.url.is_some());
@ -3021,7 +2969,7 @@ mod tests {
#[test]
fn cursor_client_capabilities_include_parameterized_model_picker_meta() {
let capabilities = client_capabilities_for_agent(&AgentId::new(CURSOR_ID), false);
let capabilities = client_capabilities_for_agent(&AgentId::new(CURSOR_ID));
let meta = capabilities
.meta
.expect("expected client capabilities meta");
@ -3036,7 +2984,7 @@ mod tests {
#[test]
fn non_cursor_client_capabilities_do_not_include_parameterized_model_picker_meta() {
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"), false);
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"));
let meta = capabilities
.meta
.expect("expected client capabilities meta");
@ -3045,8 +2993,8 @@ mod tests {
}
#[test]
fn client_capabilities_include_boolean_config_options_when_supported() {
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"), true);
fn client_capabilities_include_boolean_config_options() {
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"));
assert!(
capabilities
@ -3057,13 +3005,6 @@ mod tests {
);
}
#[test]
fn client_capabilities_omit_boolean_config_options_when_unsupported() {
let capabilities = client_capabilities_for_agent(&AgentId::new("codex-acp"), false);
assert!(capabilities.session.is_none());
}
#[test]
fn terminal_auth_task_builds_spawn_from_prebuilt_command() {
let command = AgentServerCommand {
@ -3549,69 +3490,7 @@ mod tests {
}
#[gpui::test]
async fn default_config_options_skip_boolean_defaults_when_acp_beta_is_disabled(
cx: &mut gpui::TestAppContext,
) {
cx.update(|cx| init_settings_with_acp_beta_override(false, cx));
let (connection, set_config_requests) = connect_config_defaults_test_agent(cx).await;
connection.defaults.set(
None,
HashMap::from_iter([
(
"web_search".to_string(),
AgentConfigOptionValue::Boolean(true),
),
("mode".to_string(), AgentConfigOptionValue::from("manual")),
]),
);
let config_options = Rc::new(RefCell::new(vec![
acp::SessionConfigOption::boolean("web_search", "Web Search", false),
acp::SessionConfigOption::select(
"mode",
"Mode",
"auto",
vec![
acp::SessionConfigSelectOption::new("auto", "Auto"),
acp::SessionConfigSelectOption::new("manual", "Manual"),
],
),
]));
let mut async_cx = cx.to_async();
connection.apply_default_config_options(
&acp::SessionId::new("session-config-defaults"),
&config_options,
&mut async_cx,
);
drop(async_cx);
cx.run_until_parked();
let requests = set_config_requests
.lock()
.expect("set config requests mutex poisoned");
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].config_id, acp::SessionConfigId::new("mode"));
assert_eq!(
requests[0].value,
acp::SessionConfigOptionValue::value_id("manual")
);
let options = config_options.borrow();
assert!(
matches!(&options[0].kind, acp::SessionConfigKind::Boolean(boolean) if !boolean.current_value)
);
assert!(
matches!(&options[1].kind, acp::SessionConfigKind::Select(select) if select.current_value == acp::SessionConfigValueId::new("manual"))
);
}
#[gpui::test]
async fn default_config_options_apply_boolean_defaults_when_acp_beta_is_enabled(
cx: &mut gpui::TestAppContext,
) {
cx.update(|cx| init_settings_with_acp_beta_override(true, cx));
async fn default_config_options_apply_boolean_defaults(cx: &mut gpui::TestAppContext) {
let (connection, set_config_requests) = connect_config_defaults_test_agent(cx).await;
connection.defaults.set(
None,
@ -3654,19 +3533,6 @@ mod tests {
);
}
fn init_settings_with_acp_beta_override(enabled: bool, cx: &mut App) {
let mut store = settings::SettingsStore::test(cx);
store.register_setting::<feature_flags::FeatureFlagsSettings>();
store.update_user_settings(cx, |content| {
content.feature_flags.get_or_insert_default().insert(
AcpBetaFeatureFlag::NAME.to_string(),
if enabled { "on" } else { "off" }.to_string(),
);
});
cx.set_global(store);
cx.update_flags(false, Vec::new());
}
async fn connect_config_defaults_test_agent(
cx: &mut gpui::TestAppContext,
) -> (
@ -4653,13 +4519,6 @@ fn handle_create_elicitation(
cx: &mut AsyncApp,
ctx: &ClientContext,
) {
if !cx.update(|cx| cx.has_flag::<AcpBetaFeatureFlag>()) {
return respond_err(
responder,
acp::Error::invalid_params().data("elicitation support requires the ACP beta flag"),
);
}
match args.scope() {
acp::ElicitationScope::Session(scope) => {
let thread = match session_thread(ctx, &scope.session_id) {
@ -4748,10 +4607,6 @@ fn handle_complete_elicitation(
cx: &mut AsyncApp,
ctx: &ClientContext,
) {
if !cx.update(|cx| cx.has_flag::<AcpBetaFeatureFlag>()) {
return;
}
let threads = ctx
.sessions
.borrow()

View file

@ -414,7 +414,7 @@ impl Default for AgentProfileId {
/// combines them with the in-memory per-thread grants. `write_paths` are
/// stored as minimal, lexically-normalized subtrees (see
/// [`compile_sandbox_permissions`]).
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SandboxPermissions {
/// Allow sandboxed commands to reach any host over the network.
pub allow_all_hosts: bool,
@ -432,6 +432,24 @@ pub struct SandboxPermissions {
/// tool/prompt in place — see `agent::sandboxing`.
pub allow_unsandboxed: bool,
pub write_paths: Vec<PathBuf>,
/// Whether sandbox escalation prompts warn about domains or write paths
/// that contain potentially confusable Unicode characters (homoglyphs,
/// invisible characters, or bidirectional overrides). Enabled by default.
pub warn_confusable_unicode: bool,
}
impl Default for SandboxPermissions {
fn default() -> Self {
Self {
allow_all_hosts: false,
network_hosts: Vec::new(),
allow_fs_write_all: false,
allow_unsandboxed: false,
write_paths: Vec::new(),
// The confusable-Unicode warning is a safety net, so it defaults on.
warn_confusable_unicode: true,
}
}
}
#[derive(Clone, Debug, Default)]
@ -821,6 +839,7 @@ fn compile_sandbox_permissions(
allow_fs_write_all: content.allow_fs_write_all.unwrap_or(false),
allow_unsandboxed: content.allow_unsandboxed.unwrap_or(false),
write_paths,
warn_confusable_unicode: content.warn_confusable_unicode.unwrap_or(true),
}
}
@ -1098,6 +1117,22 @@ mod tests {
fn test_sandbox_permissions_empty() {
let permissions = compile_sandbox_permissions(None);
assert_eq!(permissions, SandboxPermissions::default());
// The confusable-Unicode warning is a safety net, so it's on by default.
assert!(permissions.warn_confusable_unicode);
}
#[test]
fn test_sandbox_permissions_warn_confusable_unicode_can_be_disabled() {
let content: settings::SandboxPermissionsContent =
serde_json::from_value(json!({ "warn_confusable_unicode": false })).unwrap();
let permissions = compile_sandbox_permissions(Some(content));
assert!(!permissions.warn_confusable_unicode);
// Omitting the key keeps the warning enabled.
let content: settings::SandboxPermissionsContent =
serde_json::from_value(json!({})).unwrap();
let permissions = compile_sandbox_permissions(Some(content));
assert!(permissions.warn_confusable_unicode);
}
#[test]

View file

@ -63,6 +63,7 @@ gpui.workspace = true
gpui_tokio.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
idna.workspace = true
indoc.workspace = true
itertools.workspace = true
jsonschema.workspace = true
@ -108,6 +109,7 @@ theme_settings.workspace = true
time.workspace = true
ui.workspace = true
ui_input.workspace = true
unicode-script.workspace = true
unicode-segmentation.workspace = true
url.workspace = true
util.workspace = true
@ -143,6 +145,7 @@ remote_server = { workspace = true, features = ["test-support"] }
search = { workspace = true, features = ["test-support"] }
semver.workspace = true
shlex.workspace = true
reqwest_client.workspace = true
tempfile.workspace = true
vim.workspace = true

View file

@ -6,8 +6,8 @@ use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet};
use editor::{
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
SelectionEffects, SplittableEditor, ToPoint,
DiffHunkDelegate, Direction, Editor, EditorEvent, EditorSettings, MultiBuffer,
MultiBufferSnapshot, ResolvedDiffHunks, SelectionEffects, SplittableEditor, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
multibuffer_context_lines,
scroll::Autoscroll,
@ -28,12 +28,12 @@ use std::{
ops::Range,
sync::Arc,
};
use ui::{CommonAnimationExt, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::ResultExt;
use ui::{CommonAnimationExt, Divider, IconButtonShape, KeyBinding, Tooltip, prelude::*};
use util::{ResultExt, truncate_and_trailoff};
use workspace::{
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
Workspace,
item::{ItemEvent, SaveOptions, TabContentParams},
item::{ItemEvent, SaveOptions, TabContentParams, TabTooltipContent},
searchable::SearchableItemHandle,
};
use zed_actions::assistant::ToggleFocus;
@ -101,8 +101,7 @@ impl AgentDiffPane {
cx,
);
diff_display_editor
.set_render_diff_hunk_controls(diff_hunk_controls(&thread, workspace.clone()), cx);
diff_display_editor.set_render_diff_hunks_as_unstaged(cx);
.set_diff_hunk_delegate(Some(agent_diff_delegate(&thread, workspace.clone())), cx);
diff_display_editor.update_editors(cx, |editor, _cx| {
editor.register_addon(AgentDiffAddon);
});
@ -529,23 +528,33 @@ impl Item for AgentDiffPane {
.update(cx, |editor, cx| editor.navigate(data, window, cx))
}
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
Some("Agent Diff".into())
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
let label_content = self.tab_content_text(params.detail.unwrap_or_default(), cx);
Label::new(label_content)
.when(!params.selected, |this| this.color(Color::Muted))
.into_any_element()
}
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent> {
let title = self.thread.read(cx).title();
Label::new(if let Some(title) = title {
format!("Review: {}", title)
} else {
"Review".to_string()
})
.color(if params.selected {
Color::Default
} else {
Color::Muted
})
.into_any_element()
Some(TabTooltipContent::Custom(Box::new(Tooltip::element({
let title = title.map(|title| title.to_string());
move |_, _| {
v_flex()
.child(Label::new(
title.clone().unwrap_or_else(|| "Review".to_string()),
))
.child(
Label::new("Agent Diff")
.color(Color::Muted)
.size(LabelSize::Small),
)
.into_any_element()
}
}))))
}
fn telemetry_event_text(&self) -> Option<&'static str> {
@ -667,8 +676,11 @@ impl Item for AgentDiffPane {
});
}
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Agent Diff".into()
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
match self.thread.read(cx).title() {
Some(title) => format!("Review: {}", truncate_and_trailoff(&title, 20)).into(),
None => "Review".into(),
}
}
}
@ -722,29 +734,68 @@ impl Render for AgentDiffPane {
}
}
fn diff_hunk_controls(
struct AgentDiffDelegate {
thread: Entity<AcpThread>,
workspace: WeakEntity<Workspace>,
}
fn agent_diff_delegate(
thread: &Entity<AcpThread>,
workspace: WeakEntity<Workspace>,
) -> editor::RenderDiffHunkControlsFn {
let thread = thread.clone();
) -> Arc<dyn DiffHunkDelegate> {
Arc::new(AgentDiffDelegate {
thread: thread.clone(),
workspace,
})
}
Arc::new(
move |row, status, hunk_range, is_created_file, line_height, editor, _, cx| {
{
render_diff_hunk_controls(
row,
status,
hunk_range,
is_created_file,
line_height,
&thread,
editor,
workspace.clone(),
cx,
)
}
},
)
impl DiffHunkDelegate for AgentDiffDelegate {
fn toggle(
&self,
_hunks: Vec<ResolvedDiffHunks>,
_editor: &mut Editor,
_window: &mut Window,
_cx: &mut Context<Editor>,
) {
}
fn stage_or_unstage(
&self,
_stage: bool,
_hunks: Vec<ResolvedDiffHunks>,
_editor: &mut Editor,
_window: &mut Window,
_cx: &mut Context<Editor>,
) {
}
fn render_hunk_controls(
&self,
row: u32,
status: &DiffHunkStatus,
hunk_range: Range<editor::Anchor>,
is_created_file: bool,
line_height: Pixels,
editor: &Entity<Editor>,
_window: &mut Window,
cx: &mut App,
) -> AnyElement {
render_diff_hunk_controls(
row,
status,
hunk_range,
is_created_file,
line_height,
&self.thread,
editor,
self.workspace.clone(),
cx,
)
}
fn render_hunk_as_staged(&self, _status: &DiffHunkStatus, _cx: &App) -> bool {
false
}
}
fn render_diff_hunk_controls(
@ -1099,7 +1150,7 @@ impl Render for AgentDiffToolbar {
}),
)
.into_any_element(),
vertical_divider().into_any_element(),
Divider::vertical().into_any_element(),
h_flex()
.gap_0p5()
.child(
@ -1141,7 +1192,7 @@ impl Render for AgentDiffToolbar {
.mr_1()
.gap_1()
.children(content)
.child(vertical_divider())
.child(Divider::vertical())
.when_some(editor.read(cx).workspace(), |this, _workspace| {
this.child(
IconButton::new("review", IconName::ListTodo)
@ -1158,7 +1209,7 @@ impl Render for AgentDiffToolbar {
}),
)
})
.child(vertical_divider())
.child(Divider::vertical())
.on_action({
let editor = editor.clone();
move |_action: &OpenAgentDiff, window, cx| {
@ -1528,7 +1579,7 @@ impl AgentDiff {
for (editor, _) in self.reviewing_editors.drain() {
editor
.update(cx, |editor, cx| {
editor.end_temporary_diff_override(cx);
editor.set_diff_hunk_delegate(None, cx);
editor.unregister_addon::<EditorAgentDiffAddon>();
})
.ok();
@ -1577,12 +1628,10 @@ impl AgentDiff {
if previous_state.is_none() {
editor.update(cx, |editor, cx| {
editor.start_temporary_diff_override();
editor.set_render_diff_hunk_controls(
diff_hunk_controls(&thread, workspace.clone()),
editor.set_diff_hunk_delegate(
Some(agent_diff_delegate(&thread, workspace.clone())),
cx,
);
editor.set_render_diff_hunks_as_unstaged(true, cx);
editor.set_expand_all_diff_hunks(cx);
editor.register_addon(EditorAgentDiffAddon);
});
@ -1629,7 +1678,7 @@ impl AgentDiff {
if in_workspace {
editor
.update(cx, |editor, cx| {
editor.end_temporary_diff_override(cx);
editor.set_diff_hunk_delegate(None, cx);
editor.unregister_addon::<EditorAgentDiffAddon>();
})
.ok();

View file

@ -26,7 +26,7 @@ use zed_actions::{
agent::{
AddSelectionToThread, ConflictContent, LogoutAgent, OpenSettings, ReauthenticateAgent,
ResetAgentZoom, ResetOnboarding, ResolveConflictedFilesWithAgent,
ResolveConflictsWithAgent, ReviewBranchDiff,
ResolveConflictsWithAgent, ReviewBranchDiff, SelectAgent,
},
assistant::{
FocusAgent, ManageSkills, OpenGlobalAgentsMdRules, OpenProjectAgentsMdRules, Toggle,
@ -85,6 +85,7 @@ use project::{Project, ProjectPath, Worktree};
use settings::TerminalDockPosition;
use settings::{NotifyWhenAgentWaiting, Settings, update_settings_file};
use search::{BufferSearchBar, buffer_search::Deploy as DeployBufferSearch};
use terminal::{Event as TerminalEvent, terminal_settings::TerminalSettings};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use text::OffsetRangeExt;
@ -96,9 +97,9 @@ use ui::{
use util::ResultExt as _;
use workspace::{
CollaboratorId, DraggedSelection, DraggedTab, MultiWorkspace, PathList, SerializedPathList,
ToggleWorkspaceSidebar, ToggleZoom, Workspace, WorkspaceId,
ToggleWorkspaceSidebar, ToggleZoom, ToolbarItemView, Workspace, WorkspaceId,
dock::{DockPosition, Panel, PanelEvent},
item::ItemEvent,
item::{ItemEvent, ItemHandle},
};
const AGENT_PANEL_KEY: &str = "agent_panel";
@ -422,6 +423,14 @@ pub fn init(cx: &mut App) {
});
}
})
.register_action(|workspace, action: &SelectAgent, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
let agent = AgentId::new(action.agent.clone()).into();
panel.select_agent(agent, window, cx);
});
}
})
.register_action(|workspace, action: &ManageSkills, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@ -991,6 +1000,7 @@ struct AgentTerminal {
working_directory: Option<PathBuf>,
created_at: DateTime<Utc>,
has_notification: bool,
search_bar: Option<Entity<BufferSearchBar>>,
notification_windows: Vec<WindowHandle<AgentNotification>>,
notification_subscriptions: Vec<Subscription>,
_subscriptions: Vec<Subscription>,
@ -1911,6 +1921,43 @@ impl AgentPanel {
self.activate_new_thread(true, AgentThreadSource::AgentPanel, window, cx);
}
fn set_selected_agent_and_persist(&mut self, agent: Agent, cx: &mut Context<Self>) {
if self.selected_agent != agent {
self.selected_agent = agent.clone();
self.serialize(cx);
}
cx.background_spawn({
let kvp = KeyValueStore::global(cx);
async move {
write_global_last_used_agent(kvp, agent).await;
}
})
.detach();
}
/// Sets the panel's selected agent without opening the panel or focusing
/// it, so the agent is launched the next time the panel is opened (or
/// right away, if the panel is already showing the empty new-thread
/// draft).
pub fn select_agent(&mut self, agent: Agent, window: &mut Window, cx: &mut Context<Self>) {
if self.project.read(cx).is_via_collab() && !agent.is_native() {
return;
}
let showing_new_draft = matches!(
(&self.base_view, &self.draft_thread),
(BaseView::AgentThread { conversation_view }, Some(draft))
if conversation_view.entity_id() == draft.entity_id()
);
if matches!(self.base_view, BaseView::AgentThread { .. }) && showing_new_draft {
self.set_selected_agent_and_persist(agent, cx);
self.activate_draft(false, AgentThreadSource::AgentPanel, window, cx);
cx.notify();
}
}
pub fn new_terminal(
&mut self,
workspace: Option<&Workspace>,
@ -1990,6 +2037,7 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.pending_terminal_spawn = Some(terminal_id);
let terminal_working_directory = working_directory.clone();
let init_command = Self::terminal_init_command(run_init_command, cx);
let terminal_task = self.project.update(cx, |project, cx| {
@ -2174,6 +2222,7 @@ impl AgentPanel {
working_directory,
created_at: created_at.unwrap_or_else(Utc::now),
has_notification: false,
search_bar: None,
notification_windows: Vec::new(),
notification_subscriptions: Vec::new(),
_subscriptions: vec![view_subscription, terminal_subscription],
@ -3177,18 +3226,7 @@ impl AgentPanel {
cx,
);
if let Some(original) = saved_selected_agent {
if self.selected_agent != original {
self.selected_agent = original.clone();
self.serialize(cx);
// Restore the last-used-agent in persistent storage as well.
cx.background_spawn({
let kvp = KeyValueStore::global(cx);
async move {
write_global_last_used_agent(kvp, original).await;
}
})
.detach();
}
self.set_selected_agent_and_persist(original, cx);
}
let thread_id = thread.conversation_view.read(cx).thread_id;
self.retained_threads
@ -3949,6 +3987,37 @@ impl AgentPanel {
}
}
fn toggle_terminal_thread_search(
&mut self,
_: &crate::ToggleSearch,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(terminal) = self
.active_terminal_id()
.and_then(|terminal_id| self.terminals.get_mut(&terminal_id))
else {
cx.propagate();
return;
};
let terminal_view = terminal.view.clone();
let search_bar = terminal
.search_bar
.get_or_insert_with(|| cx.new(|cx| BufferSearchBar::new(None, window, cx)))
.clone();
let deployed = search_bar.update(cx, |search_bar, cx| {
let terminal_item: &dyn ItemHandle = &terminal_view;
search_bar.set_active_pane_item(Some(terminal_item), window, cx);
search_bar.deploy(&DeployBufferSearch::find(), None, window, cx)
});
if deployed {
cx.stop_propagation();
} else {
cx.propagate();
}
}
pub fn conversation_view_for_id(
&self,
thread_id: &ThreadId,
@ -4467,19 +4536,7 @@ impl AgentPanel {
let workspace = self.workspace.clone();
let project = self.project.clone();
if self.selected_agent != agent {
self.selected_agent = agent.clone();
self.serialize(cx);
}
cx.background_spawn({
let kvp = KeyValueStore::global(cx);
let agent = agent.clone();
async move {
write_global_last_used_agent(kvp, agent).await;
}
})
.detach();
self.set_selected_agent_and_persist(agent.clone(), cx);
let server = server_override
.unwrap_or_else(|| agent.server(self.fs.clone(), self.thread_store.clone()));
@ -5499,6 +5556,10 @@ impl AgentPanel {
.is_some_and(|thread| !thread.read(cx).is_generating_title())
});
let has_thread_messages = conversation_view.as_ref().is_some_and(|conversation_view| {
conversation_view.read(cx).has_user_submitted_prompt(cx)
});
let has_auth_methods = match &self.base_view {
BaseView::AgentThread { conversation_view } => {
conversation_view.read(cx).has_auth_methods()
@ -5534,15 +5595,15 @@ impl AgentPanel {
.with_handle(self.agent_panel_menu_handle.clone())
.menu({
move |window, cx| {
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
menu = menu.context(menu_action_context.clone());
if can_regenerate_thread_title {
if has_thread_messages {
menu = menu.header("Current Thread");
if let Some(conversation_view) = conversation_view.as_ref() {
menu = menu
.entry("Regenerate Thread Title", None, {
if can_regenerate_thread_title {
menu = menu.entry("Regenerate Thread Title", None, {
let conversation_view = conversation_view.clone();
let workspace = workspace.clone();
move |_, cx| {
@ -5552,8 +5613,29 @@ impl AgentPanel {
cx,
);
}
})
.separator();
});
}
let root_thread_view =
conversation_view.read(cx).root_thread_view();
if let Some(thread_view) = root_thread_view {
let workspace = workspace.clone();
menu = menu.entry("Open Thread as Markdown", None, {
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
thread_view.update(cx, |thread_view, cx| {
thread_view
.open_thread_as_markdown(
workspace, window, cx,
)
.detach_and_log_err(cx);
});
}
}
});
}
menu = menu.separator();
}
}
@ -5631,11 +5713,11 @@ impl AgentPanel {
},
);
}
menu = menu.separator();
}
menu = menu.action("Profiles", Box::new(ManageProfiles::default()));
menu = menu
.separator()
.action("Profiles", Box::new(ManageProfiles::default()));
}
menu = menu
@ -6372,6 +6454,7 @@ impl Render for AgentPanel {
.on_action(cx.listener(Self::decrease_font_size))
.on_action(cx.listener(Self::reset_font_size))
.on_action(cx.listener(Self::toggle_zoom))
.on_action(cx.listener(Self::toggle_terminal_thread_search))
.on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
if let Some(conversation_view) = this.active_conversation_view() {
conversation_view.update(cx, |conversation_view, cx| {
@ -6396,9 +6479,34 @@ impl Render for AgentPanel {
VisibleSurface::AgentThread(conversation_view) => parent
.child(conversation_view.clone())
.child(self.render_drag_target(cx)),
VisibleSurface::Terminal(terminal_view) => parent
.child(terminal_view.clone())
.child(self.render_drag_target(cx)),
VisibleSurface::Terminal(terminal_view) => {
let search_bar = self
.active_terminal_id()
.and_then(|terminal_id| self.terminals.get(&terminal_id))
.and_then(|terminal| terminal.search_bar.clone());
let terminal_content = v_flex()
.size_full()
.when_some(search_bar, |this, search_bar| {
this.when(!search_bar.read(cx).is_dismissed(), |this| {
this.child(
v_flex()
.group("toolbar")
.relative()
.py(DynamicSpacing::Base06.rems(cx))
.px(DynamicSpacing::Base08.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().toolbar_background)
.child(search_bar),
)
})
})
.child(terminal_view.clone());
parent
.child(terminal_content)
.child(self.render_drag_target(cx))
}
})
.children(self.render_trial_end_upsell(window, cx));
@ -7256,6 +7364,34 @@ mod tests {
});
}
#[gpui::test]
async fn test_explicit_terminal_blocks_redundant_auto_init(cx: &mut TestAppContext) {
let (panel, mut cx) = setup_panel(cx).await;
panel.update_in(&mut cx, |panel, window, cx| {
panel.last_created_entry_kind = AgentPanelEntryKind::Terminal;
assert!(
matches!(panel.base_view, BaseView::Uninitialized),
"precondition: the panel starts uninitialized"
);
assert!(
panel.pending_terminal_spawn.is_none(),
"precondition: no terminal spawn is in-flight yet"
);
panel.new_terminal(None, AgentThreadSource::AgentPanel, window, cx);
let pending = panel.pending_terminal_spawn;
assert!(
pending.is_some(),
"an explicit new terminal must mark a spawn in-flight"
);
panel.set_active(true, window, cx);
assert_eq!(
panel.pending_terminal_spawn, pending,
"activating the panel while a terminal spawn is in-flight must not schedule a second (auto-init) terminal"
);
});
}
#[gpui::test]
async fn test_restored_terminal_runs_init_command_once(cx: &mut TestAppContext) {
let (panel, mut cx) = setup_panel(cx).await;
@ -9038,7 +9174,7 @@ mod tests {
let mut text = String::new();
for path in paths {
text.push(' ');
text.push_str(&format!("{path:?}"));
text.push_str(&shlex::try_quote(path.to_str().unwrap()).unwrap());
}
text.push(' ');
text
@ -11214,6 +11350,60 @@ mod tests {
});
}
#[gpui::test]
async fn test_select_agent_action_updates_visible_draft(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
cx.update(|cx| {
agent::ThreadStore::init_global(cx);
language_model::LanguageModelRegistry::test(cx);
<dyn fs::Fs>::set_global(fs.clone(), cx);
});
fs.insert_tree("/project", json!({ "file.txt": "" })).await;
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
let multi_workspace =
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace
.read_with(cx, |multi_workspace, _cx| {
multi_workspace.workspace().clone()
})
.unwrap();
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
workspace.add_panel(panel.clone(), window, cx);
panel
});
panel.update_in(cx, |panel, window, cx| {
panel.activate_draft(false, AgentThreadSource::AgentPanel, window, cx);
});
cx.dispatch_action(SelectAgent {
agent: "my-configured-agent".to_string(),
});
cx.run_until_parked();
let expected_agent = Agent::Custom {
id: "my-configured-agent".into(),
};
panel.read_with(cx, |panel, cx| {
let draft = panel.draft_thread.as_ref().expect("draft should exist");
assert_eq!(panel.selected_agent, expected_agent);
assert_eq!(*draft.read(cx).agent_key(), expected_agent);
});
let kvp = cx.update(|_, cx| KeyValueStore::global(cx));
assert_eq!(
read_global_last_used_agent(&kvp),
Some(expected_agent),
"the selection should be persisted as the global last-used agent"
);
}
#[gpui::test]
async fn test_workspaces_maintain_independent_agent_selection(cx: &mut TestAppContext) {
init_test(cx);

View file

@ -514,19 +514,27 @@ impl AgentRegistryPage {
.size(IconSize::Small)
.color(Color::Muted),
)
.on_click(move |_, _, cx| {
let agent_id = agent_id.clone();
update_settings_file(fs.clone(), cx, move |settings, _| {
let agent_servers = settings.agent_servers.get_or_insert_default();
agent_servers.entry(agent_id).or_insert_with(|| {
settings::CustomAgentServerSettings::Registry {
default_mode: None,
env: Default::default(),
default_config_options: HashMap::default(),
favorite_config_option_values: HashMap::default(),
}
});
.on_click(move |_, window, cx| {
update_settings_file(fs.clone(), cx, {
let agent_id = agent_id.clone();
move |settings, _| {
let agent_servers = settings.agent_servers.get_or_insert_default();
agent_servers.entry(agent_id).or_insert_with(|| {
settings::CustomAgentServerSettings::Registry {
default_mode: None,
env: Default::default(),
default_config_options: HashMap::default(),
favorite_config_option_values: HashMap::default(),
}
});
}
});
window.dispatch_action(
Box::new(zed_actions::agent::SelectAgent {
agent: agent_id.clone(),
}),
cx,
);
})
}
RegistryInstallStatus::InstalledRegistry => {

View file

@ -35,6 +35,7 @@ pub mod thread_worktree_archive;
pub mod threads_archive_view;
mod ui;
mod unicode_confusables;
use std::rc::Rc;
use std::sync::Arc;
@ -47,8 +48,8 @@ use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use feature_flags::FeatureFlagAppExt as _;
use fs::Fs;
use gpui::{
Action, App, Context, Entity, ImageSource, Resource, SharedString, SharedUri, TaskExt, Window,
actions,
Action, App, Context, Entity, ImageSource, ReadGlobal as _, Resource, SharedString, SharedUri,
TaskExt, Window, actions,
};
use language::{
LanguageRegistry,
@ -65,7 +66,7 @@ use serde::{Deserialize, Serialize};
use settings::{LanguageModelSelection, Settings as _, SettingsStore, SidebarSide};
use std::any::TypeId;
use std::path::{Path, PathBuf};
use workspace::Workspace;
use workspace::{OpenOptions, Workspace};
use crate::agent_configuration::ManageProfilesModal;
pub use crate::agent_connection_store::{ActiveAcpConnection, AgentConnectionStore};
@ -118,40 +119,68 @@ pub(crate) fn resolve_agent_image(
None
}
/// Opens `abs_path` in the workspace, moving the cursor to `point` when one
/// is given. Paths outside every worktree are only opened when a file exists
/// there, so broken agent links don't create empty buffers.
pub(crate) fn open_abs_path_at_point(
workspace: &mut Workspace,
abs_path: PathBuf,
point: Point,
point: Option<Point>,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> bool {
let project = workspace.project();
let Some(path) = project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
else {
return false;
};
let item = workspace.open_path(path, None, true, window, cx);
) {
let project_path = workspace
.project()
.update(cx, |project, cx| project.find_project_path(&abs_path, cx));
let fs = workspace.project().read(cx).fs().clone();
let workspace = cx.weak_entity();
window
.spawn(cx, async move |cx| {
let Some(editor) = item.await?.downcast::<Editor>() else {
let item = if let Some(project_path) = project_path {
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(project_path, None, true, window, cx)
})?
.await?
} else {
let metadata = fs.metadata(&abs_path).await?;
anyhow::ensure!(
metadata.is_some_and(|metadata| !metadata.is_dir),
"no file found at path {abs_path:?}"
);
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_abs_path(
abs_path,
OpenOptions {
focus: Some(true),
..Default::default()
},
window,
cx,
)
})?
.await?
};
let Some(point) = point else {
return Ok(());
};
let Some(editor) = item.downcast::<Editor>() else {
return Ok(());
};
let range = point..point;
editor
.update_in(cx, |editor, window, cx| {
editor.change_selections(
SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|selections| selections.select_ranges([range]),
|selections| selections.select_ranges([point..point]),
);
})
.ok();
anyhow::Ok(())
})
.detach_and_log_err(cx);
true
}
pub const DEFAULT_THREAD_TITLE: &str = "New Agent Thread";
@ -857,11 +886,12 @@ fn init_language_model_settings(cx: &mut App) {
.detach();
cx.subscribe(
&LanguageModelRegistry::global(cx),
|_, event: &language_model::Event, cx| match event {
|registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
registry.update(cx, |registry, cx| registry.refresh_fallback_model(cx));
update_active_language_model_from_settings(cx);
}
_ => {}
@ -880,6 +910,12 @@ fn update_active_language_model_from_settings(cx: &mut App) {
}
}
let should_use_fallback = SettingsStore::global(cx)
.raw_user_settings()
.and_then(|user| user.content.agent.as_ref())
.and_then(|agent| agent.default_model.as_ref())
.is_none();
let default = settings.default_model.as_ref().map(to_selected_model);
let inline_assistant = settings
.inline_assistant_model
@ -905,6 +941,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
registry.select_commit_message_model(commit_message.as_ref(), cx);
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
registry.select_inline_alternative_models(inline_alternatives, cx);
registry.set_should_use_fallback(should_use_fallback);
});
}

View file

@ -525,18 +525,18 @@ impl CodegenAlternative {
messages.push(user_message);
let tools = vec![
LanguageModelRequestTool {
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(),
use_input_streaming: false,
},
LanguageModelRequestTool {
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(),
use_input_streaming: false,
},
LanguageModelRequestTool::function(
REWRITE_SECTION_TOOL_NAME.to_string(),
"Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
false,
),
LanguageModelRequestTool::function(
FAILURE_MESSAGE_TOOL_NAME.to_string(),
"Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
false,
),
];
LanguageModelRequest {
@ -1179,9 +1179,7 @@ impl CodegenAlternative {
let mut chars_read_by_tool_id = chars_read_by_tool_id.lock();
match tool_use.name.as_ref() {
REWRITE_SECTION_TOOL_NAME => {
let Ok(input) =
serde_json::from_value::<RewriteSectionInput>(tool_use.input)
else {
let Ok(input) = tool_use.input.parse::<RewriteSectionInput>() else {
return None;
};
let chars_read_so_far =
@ -1198,9 +1196,7 @@ impl CodegenAlternative {
})
}
FAILURE_MESSAGE_TOOL_NAME => {
let Ok(mut input) =
serde_json::from_value::<FailureMessageInput>(tool_use.input)
else {
let Ok(mut input) = tool_use.input.parse::<FailureMessageInput>() else {
return None;
};
Some(ToolUseOutput::Failure(std::mem::take(&mut input.message)))
@ -2011,7 +2007,9 @@ mod tests {
id: id.into(),
name: REWRITE_SECTION_TOOL_NAME.into(),
raw_input: serde_json::to_string(&input).unwrap(),
input: serde_json::to_value(&input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(&input).unwrap(),
),
is_input_complete: is_complete,
thought_signature: None,
})

View file

@ -190,6 +190,51 @@ impl PromptContextAction {
}
}
/// A slash command that runs a local UI action against the conversation
/// (sending feedback) instead of being sent to the agent as part of a prompt.
/// Each variant maps to a method on `ThreadView`; the completion provider only
/// surfaces them and emits an event, while `ThreadView` performs the actual
/// work (see `handle_message_editor_event`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PromptLocalCommand {
ThumbsUp,
ThumbsDown,
}
impl PromptLocalCommand {
pub fn keyword(&self) -> &'static str {
match self {
Self::ThumbsUp => "helpful",
Self::ThumbsDown => "not-helpful",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::ThumbsUp => "Positive Feedback",
Self::ThumbsDown => "Negative Feedback",
}
}
pub fn description(&self) -> &'static str {
match self {
Self::ThumbsUp => {
"Rate this response as helpful. Sends the current conversation to the Zed team."
}
Self::ThumbsDown => {
"Rate this response as not helpful. Sends the current conversation to the Zed team."
}
}
}
pub fn icon(&self) -> IconName {
match self {
Self::ThumbsUp => IconName::ThumbsUp,
Self::ThumbsDown => IconName::ThumbsDown,
}
}
}
impl TryFrom<&str> for PromptContextType {
type Error = String;
@ -366,13 +411,15 @@ impl AvailableCommand {
enum SlashCompletionCandidate {
Skill(AvailableSkill),
Command(AvailableCommand),
LocalCommand(PromptLocalCommand),
}
impl SlashCompletionCandidate {
fn name(&self) -> &Arc<str> {
fn name(&self) -> &str {
match self {
Self::Skill(skill) => &skill.name,
Self::Command(command) => &command.name,
Self::LocalCommand(command) => command.keyword(),
}
}
}
@ -385,6 +432,7 @@ fn slash_completion_group_key(candidate: &SlashCompletionCandidate) -> u32 {
match candidate {
SlashCompletionCandidate::Skill(_) => 0,
SlashCompletionCandidate::Command(command) => 1 + command.category_order() as u32,
SlashCompletionCandidate::LocalCommand(_) => 4,
}
}
@ -411,6 +459,13 @@ pub trait PromptCompletionProviderDelegate: Send + Sync + 'static {
fn available_skills(&self, _cx: &App) -> Vec<AvailableSkill> {
Vec::new()
}
fn available_local_commands(&self, _cx: &App) -> Vec<PromptLocalCommand> {
Vec::new()
}
fn run_local_command(&self, _command: PromptLocalCommand, _cx: &mut App) {}
fn confirm_command(&self, cx: &mut App);
/// Called once each time the user opens slash-command autocomplete
@ -959,15 +1014,21 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
let mut candidates = self
.source
.available_skills(cx)
.available_commands(cx)
.into_iter()
.map(SlashCompletionCandidate::Skill)
.map(SlashCompletionCandidate::Command)
.collect::<Vec<_>>();
candidates.extend(
self.source
.available_commands(cx)
.available_skills(cx)
.into_iter()
.map(SlashCompletionCandidate::Command),
.map(SlashCompletionCandidate::Skill),
);
candidates.extend(
self.source
.available_local_commands(cx)
.into_iter()
.map(SlashCompletionCandidate::LocalCommand),
);
if candidates.is_empty() {
return Task::ready(Vec::new());
@ -1429,7 +1490,10 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
Some((new_text, icon_path, icon_color, confirm)),
)
}
SlashCompletionCandidate::Command(_) => (candidate, None),
SlashCompletionCandidate::Command(_)
| SlashCompletionCandidate::LocalCommand(_) => {
(candidate, None)
}
})
.collect::<Vec<(SlashCompletionCandidate, Option<SkillInfo>)>>()
})
@ -1542,6 +1606,50 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
group,
}
}
SlashCompletionCandidate::LocalCommand(command) => {
let group = show_section_headers.then(|| CompletionGroup {
key: "local-commands".into(),
label: Some("Actions".into()),
});
Completion {
replace_range: source_range.clone(),
// Local commands aren't part of the prompt;
// confirming one clears the typed text
// rather than leaving `/keyword` behind.
new_text: String::new(),
label: CodeLabel::plain(command.label().to_string(), None),
documentation: Some(
CompletionDocumentation::MultiLinePlainText(
command.description().into(),
),
),
source: project::CompletionSource::Custom,
icon_path: Some(command.icon().path().into()),
icon_color: None,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(Arc::new({
let source = source.clone();
move |intent, _window, cx| {
cx.defer({
let source = source.clone();
move |cx| match intent {
CompletionIntent::Complete
| CompletionIntent::CompleteWithInsert
| CompletionIntent::CompleteWithReplace => {
source.run_local_command(command, cx);
}
CompletionIntent::Compose => {}
}
});
false
}
})),
group,
}
}
})
.collect();

View file

@ -5,7 +5,6 @@ use agent_client_protocol::schema::v1 as acp;
use agent_servers::AgentServer;
use collections::HashSet;
use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
use fs::Fs;
use fuzzy::StringMatchCandidate;
use gpui::{
@ -99,9 +98,8 @@ impl ConfigOptionsView {
favorites_only: bool,
cx: &mut Context<Self>,
) -> bool {
let render_boolean_config_options = should_render_boolean_config_options(cx);
let Some(config_id) = self.first_config_option_id_matching(category, |option| {
Self::can_cycle_config_option(option, favorites_only, render_boolean_config_options)
Self::can_cycle_config_option(option, favorites_only)
}) else {
return false;
};
@ -144,14 +142,10 @@ impl ConfigOptionsView {
.map(|option| option.id)
}
fn can_cycle_config_option(
option: &acp::SessionConfigOption,
favorites_only: bool,
render_boolean_config_options: bool,
) -> bool {
fn can_cycle_config_option(option: &acp::SessionConfigOption, favorites_only: bool) -> bool {
match &option.kind {
acp::SessionConfigKind::Select(_) => true,
acp::SessionConfigKind::Boolean(_) => !favorites_only && render_boolean_config_options,
acp::SessionConfigKind::Boolean(_) => !favorites_only,
_ => false,
}
}
@ -213,7 +207,7 @@ impl ConfigOptionsView {
))
}
acp::SessionConfigKind::Boolean(boolean) => {
if favorites_only || !should_render_boolean_config_options(cx) {
if favorites_only {
None
} else {
Some(acp::SessionConfigOptionValue::boolean(
@ -545,10 +539,6 @@ impl Render for ConfigOptionSelector {
.into_any_element()
}
acp::SessionConfigKind::Boolean(boolean) => {
if !should_render_boolean_config_options(cx) {
return div().into_any_element();
}
let option_id = option.id.clone();
let option_name: SharedString = option.name.clone().into();
let option_description: Option<SharedString> =
@ -991,10 +981,6 @@ fn setting_value_for_config_option_value(
}
}
fn should_render_boolean_config_options(cx: &App) -> bool {
cx.has_flag::<AcpBetaFeatureFlag>()
}
fn options_to_picker_entries(
options: &[ConfigOptionValue],
favorites: &HashSet<acp::SessionConfigValueId>,
@ -1110,9 +1096,8 @@ fn count_config_options(option: &acp::SessionConfigOption) -> usize {
mod tests {
use super::*;
use acp_thread::AgentConnection;
use feature_flags::FeatureFlag as _;
use fs::FakeFs;
use gpui::{TestAppContext, UpdateGlobal};
use gpui::TestAppContext;
use parking_lot::Mutex;
use project::{AgentId, Project};
use std::{any::Any, cell::RefCell};
@ -1178,9 +1163,6 @@ mod tests {
let fs: Arc<dyn Fs> = FakeFs::new(cx.executor());
cx.update(|cx| {
init_feature_flag_settings(cx);
set_feature_flag_override(AcpBetaFeatureFlag::NAME, "on", cx);
let config_options: Rc<dyn AgentSessionConfigOptions> = config_options.clone();
let agent_server: Rc<dyn AgentServer> = agent_server.clone();
let fs = fs.clone();
@ -1215,41 +1197,7 @@ mod tests {
}
#[gpui::test]
fn cycling_hidden_boolean_config_option_is_unhandled(cx: &mut TestAppContext) {
let agent_server = Rc::new(TestAgentServer::default());
let config_options = Rc::new(TestSessionConfigOptions::new(vec![
acp::SessionConfigOption::boolean("web_search", "Web Search", false)
.category(acp::SessionConfigOptionCategory::ModelConfig),
]));
let fs: Arc<dyn Fs> = FakeFs::new(cx.executor());
cx.update(|cx| {
init_feature_flag_settings(cx);
set_feature_flag_override(AcpBetaFeatureFlag::NAME, "off", cx);
let config_options: Rc<dyn AgentSessionConfigOptions> = config_options.clone();
let agent_server: Rc<dyn AgentServer> = agent_server.clone();
let fs = fs.clone();
let view = cx.new(|_| ConfigOptionsView {
config_option_ids: ConfigOptionsView::config_option_ids(&config_options),
config_options,
selectors: Vec::new(),
agent_server,
fs,
_refresh_task: Task::ready(()),
});
assert!(!view.update(cx, |view, cx| {
view.cycle_category_option(acp::SessionConfigOptionCategory::ModelConfig, false, cx)
}));
});
assert!(agent_server.saved_defaults.lock().is_empty());
assert!(config_options.set_values.borrow().is_empty());
}
#[gpui::test]
fn cycling_category_skips_hidden_boolean_config_option(cx: &mut TestAppContext) {
fn cycling_category_cycles_boolean_config_option_first(cx: &mut TestAppContext) {
let agent_server = Rc::new(TestAgentServer::default());
let config_options = Rc::new(TestSessionConfigOptions::new(vec![
acp::SessionConfigOption::boolean("web_search", "Web Search", false)
@ -1268,9 +1216,6 @@ mod tests {
let fs: Arc<dyn Fs> = FakeFs::new(cx.executor());
cx.update(|cx| {
init_feature_flag_settings(cx);
set_feature_flag_override(AcpBetaFeatureFlag::NAME, "off", cx);
let config_options: Rc<dyn AgentSessionConfigOptions> = config_options.clone();
let agent_server: Rc<dyn AgentServer> = agent_server.clone();
let fs = fs.clone();
@ -1291,15 +1236,15 @@ mod tests {
assert_eq!(
agent_server.saved_defaults.lock().as_slice(),
&[(
"model".to_string(),
Some(AgentConfigOptionValue::ValueId("large".to_string()))
"web_search".to_string(),
Some(AgentConfigOptionValue::Boolean(true))
)]
);
assert_eq!(
config_options.set_values.borrow().as_slice(),
&[(
"model".to_string(),
acp::SessionConfigOptionValue::value_id("large")
"web_search".to_string(),
acp::SessionConfigOptionValue::boolean(true)
)]
);
}
@ -1330,45 +1275,11 @@ mod tests {
assert!(!handled);
}
#[gpui::test]
fn boolean_config_option_rendering_is_beta_gated(cx: &mut TestAppContext) {
cx.update(|cx| {
init_feature_flag_settings(cx);
cx.update_flags(false, Vec::new());
set_feature_flag_override(AcpBetaFeatureFlag::NAME, "off", cx);
assert!(!should_render_boolean_config_options(cx));
set_feature_flag_override(AcpBetaFeatureFlag::NAME, "on", cx);
assert!(should_render_boolean_config_options(cx));
});
}
#[derive(Default)]
struct TestAgentServer {
saved_defaults: Arc<Mutex<Vec<(String, Option<AgentConfigOptionValue>)>>>,
}
fn init_feature_flag_settings(cx: &mut App) {
let store = SettingsStore::test(cx);
cx.set_global(store);
SettingsStore::update_global(cx, |store, _| {
store.register_setting::<feature_flags::FeatureFlagsSettings>();
});
cx.update_flags(false, Vec::new());
}
fn set_feature_flag_override(name: &str, value: &str, cx: &mut App) {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |content| {
content
.feature_flags
.get_or_insert_default()
.insert(name.to_string(), value.to_string());
});
});
}
impl AgentServer for TestAgentServer {
fn logo(&self) -> IconName {
IconName::ZedAssistant

View file

@ -23,19 +23,18 @@ use editor::scroll::Autoscroll;
use editor::{
Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior,
};
use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
use file_icons::FileIcons;
use fs::Fs;
use futures::FutureExt as _;
use gpui::{
Action, Animation, AnimationExt, AnyView, App, ClickEvent, ClipboardItem, CursorStyle,
ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla, ListOffset, ListState,
ObjectFit, PlatformDisplay, ScrollHandle, SharedString, StyledText, Subscription, Task,
TextRun, TextStyle, WeakEntity, Window, WindowHandle, div, ease_in_out, img, linear_color_stop,
Action, Animation, AnimationExt, App, ClickEvent, ClipboardItem, CursorStyle, ElementId, Empty,
Entity, EventEmitter, FocusHandle, Focusable, Hsla, ListOffset, ListState, ObjectFit,
PlatformDisplay, ScrollHandle, SharedString, StyledText, Subscription, Task, TextRun,
TextStyle, WeakEntity, Window, WindowHandle, div, ease_in_out, img, linear_color_stop,
linear_gradient, list, pulsating_between,
};
use language::{Buffer, Language, Rope};
use language_model::{LanguageModelCompletionError, LanguageModelRegistry};
use language_model::LanguageModelCompletionError;
use markdown::{
CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont, MarkdownStyle,
};
@ -277,7 +276,7 @@ impl Conversation {
let session_id = thread.read(cx).session_id().clone();
let subscription = cx.subscribe(&thread, {
let session_id = session_id.clone();
move |this, _thread, event, cx| {
move |this, _thread, event, _cx| {
this.updated_at = Some(Instant::now());
match event {
AcpThreadEvent::ToolAuthorizationRequested(id) => {
@ -294,15 +293,12 @@ impl Conversation {
}
}
}
AcpThreadEvent::ElicitationRequested(id)
if cx.has_flag::<AcpBetaFeatureFlag>() =>
{
AcpThreadEvent::ElicitationRequested(id) => {
this.elicitation_requests
.entry(session_id.clone())
.or_default()
.push(id.clone());
}
AcpThreadEvent::ElicitationRequested(_) => {}
AcpThreadEvent::ElicitationResponded(id) => {
if let Some(elicitations) = this.elicitation_requests.get_mut(&session_id) {
elicitations.retain(|elicitation_id| elicitation_id != id);
@ -422,10 +418,6 @@ impl Conversation {
response: acp::CreateElicitationResponse,
cx: &mut Context<Self>,
) -> Option<()> {
if !cx.has_flag::<AcpBetaFeatureFlag>() {
return None;
}
let thread = self.threads.get(&session_id)?.clone();
thread.update(cx, |thread, cx| {
thread.respond_to_elicitation(&elicitation_id, response, cx);
@ -758,9 +750,7 @@ enum AuthState {
Ok,
Unauthenticated {
description: Option<Entity<Markdown>>,
configuration_view: Option<AnyView>,
pending_auth_method: Option<acp::AuthMethodId>,
_subscription: Option<Subscription>,
},
}
@ -1160,14 +1150,7 @@ impl ConversationView {
Err(e) => match e.downcast::<acp_thread::AuthRequired>() {
Ok(err) => {
cx.update(|window, cx| {
Self::handle_auth_required(
this,
err,
agent.agent_id(),
connection,
window,
cx,
)
Self::handle_auth_required(this, err, connection, window, cx)
})
.log_err();
return;
@ -1449,55 +1432,17 @@ impl ConversationView {
fn handle_auth_required(
this: WeakEntity<Self>,
err: AuthRequired,
agent_id: AgentId,
connection: Rc<dyn AgentConnection>,
window: &mut Window,
cx: &mut App,
) {
let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
let registry = LanguageModelRegistry::global(cx);
let sub = window.subscribe(&registry, cx, {
let provider_id = provider_id.clone();
let this = this.clone();
move |_, ev, window, cx| {
if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
&& &provider_id == updated_provider_id
&& LanguageModelRegistry::global(cx)
.read(cx)
.provider(&provider_id)
.map_or(false, |provider| provider.is_authenticated(cx))
{
this.update(cx, |this, cx| {
this.reset(window, cx);
})
.ok();
}
}
});
let view = registry.read(cx).provider(&provider_id).map(|provider| {
provider.configuration_view(
language_model::ConfigurationViewTargetAgent::Other(agent_id.0),
window,
cx,
)
});
(view, Some(sub))
} else {
(None, None)
};
this.update(cx, |this, cx| {
let description = err
.description
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx)));
let auth_state = AuthState::Unauthenticated {
pending_auth_method: None,
configuration_view,
description,
_subscription: subscription,
};
if let Some(connected) = this.as_connected_mut() {
connected.auth_state = auth_state;
@ -1661,7 +1606,7 @@ impl ConversationView {
});
active.update(cx, |active, cx| {
active.sync_elicitation_state_for_entry(index, window, cx);
active.sync_editor_mode_for_empty_state(cx);
active.sync_editor_mode(cx);
active.sync_generating_indicator(cx);
});
}
@ -1688,7 +1633,7 @@ impl ConversationView {
entry_view_state.update(cx, |view_state, _cx| view_state.remove(range.clone()));
list_state.splice(range.clone(), 0);
active.update(cx, |active, cx| {
active.sync_editor_mode_for_empty_state(cx);
active.sync_editor_mode(cx);
});
}
}
@ -1699,10 +1644,9 @@ impl ConversationView {
self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
}
AcpThreadEvent::ToolAuthorizationReceived(_) => {}
AcpThreadEvent::ElicitationRequested(_) if cx.has_flag::<AcpBetaFeatureFlag>() => {
AcpThreadEvent::ElicitationRequested(_) => {
self.notify_with_sound("Waiting for input", IconName::Info, window, cx);
}
AcpThreadEvent::ElicitationRequested(_) => {}
AcpThreadEvent::ElicitationResponded(_) => {}
AcpThreadEvent::Retry(retry) => {
if let Some(active) = self.thread_view(&session_id) {
@ -1963,7 +1907,6 @@ impl ConversationView {
let connection = connected.connection.clone();
let AuthState::Unauthenticated {
configuration_view,
pending_auth_method,
..
} = &mut connected.auth_state
@ -1974,7 +1917,6 @@ impl ConversationView {
let agent_telemetry_id = connection.telemetry_id();
if let Some(login_task) = connection.terminal_auth_task(&method, cx) {
configuration_view.take();
pending_auth_method.replace(method.clone());
let project = self.project.clone();
@ -2043,7 +1985,6 @@ impl ConversationView {
return;
}
configuration_view.take();
pending_auth_method.replace(method.clone());
let authenticate = connection.authenticate(method, cx);
@ -2292,7 +2233,6 @@ impl ConversationView {
&self,
connection: &Rc<dyn AgentConnection>,
description: Option<&Entity<Markdown>>,
configuration_view: Option<&AnyView>,
pending_auth_method: Option<&acp::AuthMethodId>,
window: &mut Window,
cx: &Context<Self>,
@ -2305,10 +2245,8 @@ impl ConversationView {
.agent_display_name(&self.agent.agent_id())
.unwrap_or_else(|| self.agent.agent_id().0);
let show_fallback_description = auth_methods.len() > 1
&& configuration_view.is_none()
&& description.is_none()
&& pending_auth_method.is_none();
let show_fallback_description =
auth_methods.len() > 1 && description.is_none() && pending_auth_method.is_none();
let auth_buttons = || {
h_flex().justify_end().flex_wrap().gap_1().children(
@ -2383,12 +2321,7 @@ impl ConversationView {
.color(Color::Muted),
)
} else {
this.children(
configuration_view
.cloned()
.map(|view| div().w_full().child(view)),
)
.children(description.map(|desc| {
this.children(description.map(|desc| {
self.render_markdown(
desc.clone(),
MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
@ -2405,11 +2338,6 @@ impl ConversationView {
}
fn sync_request_elicitation_states(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !cx.has_flag::<AcpBetaFeatureFlag>() {
self.request_elicitation_form_states.clear();
return;
}
let Some(store) = self.request_elicitation_store() else {
self.request_elicitation_form_states.clear();
return;
@ -2455,10 +2383,6 @@ impl ConversationView {
view: WeakEntity<Self>,
cx: &App,
) -> Vec<AnyElement> {
if !cx.has_flag::<AcpBetaFeatureFlag>() {
return Vec::new();
}
let Some(store) = connection.request_elicitations() else {
return Vec::new();
};
@ -2587,10 +2511,6 @@ impl ConversationView {
_window: &mut Window,
cx: &mut Context<Self>,
) {
if !cx.has_flag::<AcpBetaFeatureFlag>() {
return;
}
let Some(store) = self.request_elicitation_store() else {
return;
};
@ -2639,10 +2559,6 @@ impl ConversationView {
_window: &mut Window,
cx: &mut Context<Self>,
) {
if !cx.has_flag::<AcpBetaFeatureFlag>() {
return;
}
self.respond_to_request_elicitation(
elicitation_id,
acp::CreateElicitationResponse::new(acp::ElicitationAction::Decline),
@ -2656,10 +2572,6 @@ impl ConversationView {
_window: &mut Window,
cx: &mut Context<Self>,
) {
if !cx.has_flag::<AcpBetaFeatureFlag>() {
return;
}
self.respond_to_request_elicitation(
elicitation_id,
acp::CreateElicitationResponse::new(acp::ElicitationAction::Cancel),
@ -3245,7 +3157,6 @@ impl ConversationView {
}
pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let agent_id = self.agent.agent_id();
self.cancel_request_elicitations(cx);
if let Some(active) = self.root_thread_view() {
active.update(cx, |active, cx| active.clear_thread_error(cx));
@ -3256,7 +3167,7 @@ impl ConversationView {
return;
};
window.defer(cx, |window, cx| {
Self::handle_auth_required(this, AuthRequired::new(), agent_id, connection, window, cx);
Self::handle_auth_required(this, AuthRequired::new(), connection, window, cx);
})
}
@ -3288,9 +3199,7 @@ impl ConversationView {
if let Some(connected) = this.as_connected_mut() {
connected.auth_state = AuthState::Unauthenticated {
description: None,
configuration_view: None,
pending_auth_method: None,
_subscription: None,
};
cx.emit(StateChange);
if let Some(view) = connected.active_view()
@ -3431,9 +3340,7 @@ impl Render for ConversationView {
auth_state:
AuthState::Unauthenticated {
description,
configuration_view,
pending_auth_method,
_subscription,
},
..
}) => v_flex()
@ -3443,7 +3350,6 @@ impl Render for ConversationView {
.child(self.render_auth_required_state(
connection,
description.as_ref(),
configuration_view.as_ref(),
pending_auth_method.as_ref(),
window,
cx,
@ -3698,7 +3604,7 @@ pub(crate) mod tests {
use agent_servers::FakeAcpAgentServer;
use editor::MultiBufferOffset;
use editor::actions::Paste;
use feature_flags::{FeatureFlag as _, FeatureFlagAppExt as _};
use feature_flags::{AcpBetaFeatureFlag, FeatureFlag as _, FeatureFlagAppExt as _};
use fs::FakeFs;
use gpui::{ClipboardItem, EventEmitter, TestAppContext, VisualTestContext, point, size};
use parking_lot::Mutex;

View file

@ -16,6 +16,7 @@ use ui::{
struct ElicitationOption {
value: String,
label: SharedString,
description: Option<SharedString>,
}
enum ElicitationFieldState {
@ -429,6 +430,51 @@ mod tests {
);
}
#[test]
fn single_select_options_include_titled_descriptions() {
let schema = acp::StringPropertySchema::new().one_of(vec![
acp::EnumOption::new("production", "Production").description("Use live resources"),
]);
let options = single_select_options(&schema);
let [option] = options.as_slice() else {
panic!("expected one option, got {}", options.len());
};
assert_eq!(option.value, "production");
assert_eq!(option.label.to_string(), "Production");
assert_eq!(
option
.description
.as_ref()
.map(|description| description.to_string()),
Some("Use live resources".to_string())
);
}
#[test]
fn multi_select_options_include_titled_descriptions() {
let schema = acp::MultiSelectPropertySchema::titled(vec![
acp::EnumOption::new("repository", "Repository Access")
.description("Read and update repositories"),
]);
let options = multi_select_options(&schema);
let [option] = options.as_slice() else {
panic!("expected one option, got {}", options.len());
};
assert_eq!(option.value, "repository");
assert_eq!(option.label.to_string(), "Repository Access");
assert_eq!(
option
.description
.as_ref()
.map(|description| description.to_string()),
Some("Read and update repositories".to_string())
);
}
#[gpui::test]
fn form_state_preserves_string_whitespace(cx: &mut TestAppContext) {
crate::conversation_view::tests::init_test(cx);
@ -773,8 +819,10 @@ fn preview_form_schema() -> acp::ElicitationSchema {
.title("Environment")
.description("Select the environment this credential should target.")
.one_of(vec![
acp::EnumOption::new("production", "Production"),
acp::EnumOption::new("staging", "Staging"),
acp::EnumOption::new("production", "Production")
.description("Use the live account and production resources."),
acp::EnumOption::new("staging", "Staging")
.description("Validate changes against staging data first."),
acp::EnumOption::new("development", "Development"),
])
.default_value("staging"),
@ -783,8 +831,10 @@ fn preview_form_schema() -> acp::ElicitationSchema {
.property(
"scopes",
acp::MultiSelectPropertySchema::titled(vec![
acp::EnumOption::new("profile", "Profile"),
acp::EnumOption::new("repository", "Repository Access"),
acp::EnumOption::new("profile", "Profile")
.description("Read account identity and basic profile details."),
acp::EnumOption::new("repository", "Repository Access")
.description("Read and update repositories connected to this account."),
acp::EnumOption::new("terminal", "Terminal Commands"),
])
.title("Access")
@ -810,6 +860,7 @@ fn single_select_options(schema: &acp::StringPropertySchema) -> Vec<ElicitationO
.map(|option| ElicitationOption {
value: option.value.clone(),
label: SharedString::from(option.title.clone()),
description: option.description.clone().map(SharedString::from),
})
.collect();
}
@ -822,6 +873,7 @@ fn single_select_options(schema: &acp::StringPropertySchema) -> Vec<ElicitationO
.map(|value| ElicitationOption {
value: value.clone(),
label: SharedString::from(value.clone()),
description: None,
})
.collect()
}
@ -843,12 +895,13 @@ fn single_select_default_value(
fn multi_select_options(schema: &acp::MultiSelectPropertySchema) -> Vec<ElicitationOption> {
match &schema.items {
acp::MultiSelectItems::Untitled(items) => items
acp::MultiSelectItems::String(items) => items
.values
.iter()
.map(|value| ElicitationOption {
value: value.clone(),
label: SharedString::from(value.clone()),
description: None,
})
.collect(),
acp::MultiSelectItems::Titled(items) => items
@ -857,6 +910,7 @@ fn multi_select_options(schema: &acp::MultiSelectPropertySchema) -> Vec<Elicitat
.map(|option| ElicitationOption {
value: option.value.clone(),
label: SharedString::from(option.title.clone()),
description: option.description.clone().map(SharedString::from),
})
.collect(),
_ => Vec::new(),
@ -1310,11 +1364,7 @@ impl<'a> ElicitationCard<'a> {
cx,
);
})
.child(
div()
.mt_0p5()
.child(Checkbox::new(checkbox_id, checkbox_state)),
)
.child(div().child(Checkbox::new(checkbox_id, checkbox_state)))
.child(
v_flex()
.gap_0p5()
@ -1412,8 +1462,8 @@ impl<'a> ElicitationCard<'a> {
)))
.w_full()
.min_h(rems_from_px(28.))
.items_center()
.gap_2()
.items_start()
.gap_1p5()
.rounded_sm()
.border_1()
.border_color(field_border_color.opacity(0.5))
@ -1430,8 +1480,8 @@ impl<'a> ElicitationCard<'a> {
cx,
);
})
.child(Checkbox::new(checkbox_id, checkbox_state))
.child(Label::new(option.label).size(LabelSize::Small).truncate())
.child(div().child(Checkbox::new(checkbox_id, checkbox_state)))
.child(Self::render_option_content(option))
}))
.into_any_element()
}
@ -1479,8 +1529,8 @@ impl<'a> ElicitationCard<'a> {
.id(option_id)
.w_full()
.min_h(rems_from_px(28.))
.items_center()
.gap_2()
.items_start()
.gap_1p5()
.rounded_sm()
.border_1()
.border_color(border_color.opacity(0.5))
@ -1496,16 +1546,39 @@ impl<'a> ElicitationCard<'a> {
cx,
);
})
.child(Self::render_radio_indicator(
is_selected,
border_color,
control_background,
))
.child(Label::new(option.label).size(LabelSize::Small).truncate())
.child(
div()
.size(Checkbox::container_size())
.flex_none()
.flex()
.items_center()
.justify_center()
.child(Self::render_radio_indicator(
is_selected,
border_color,
control_background,
)),
)
.child(Self::render_option_content(option))
}))
.into_any_element()
}
fn render_option_content(option: ElicitationOption) -> Div {
v_flex()
.min_w_0()
.flex_1()
.gap_0p5()
.child(Label::new(option.label).size(LabelSize::Small).truncate())
.when_some(option.description, |this, description| {
this.child(
Label::new(description)
.size(LabelSize::Small)
.color(Color::Muted),
)
})
}
fn option_row_background(is_selected: bool, cx: &App) -> Hsla {
let editor_background = cx.theme().colors().editor_background;
if is_selected {

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
use anyhow::Result;
use gpui::{App, AppContext as _, Entity, Task};
use language::{Anchor, BufferSnapshot, DiagnosticEntryRef, DiagnosticSeverity, ToOffset};
use multi_buffer::MultiBuffer;
use project::{DiagnosticSummary, Project};
use rope::Point;
use std::{fmt::Write, ops::RangeInclusive, path::Path};
@ -22,7 +23,7 @@ pub fn codeblock_fence_for_path(
write!(text, "{path}").unwrap();
} else {
write!(text, "untitled").unwrap();
write!(text, "{}", MultiBuffer::DEFAULT_TITLE).unwrap();
}
if let Some(row_range) = row_range {

View file

@ -1,11 +1,14 @@
use std::ops::Range;
use std::{ops::Range, sync::Arc};
use acp_thread::{AcpThread, AgentThreadEntry, AssistantMessageChunk};
use agent::ThreadStore;
use agent_client_protocol::schema::v1 as acp;
use agent_settings::AgentSettings;
use collections::{HashMap, HashSet};
use editor::{Editor, EditorEvent, EditorMode, MinimapVisibility, SizingBehavior};
use editor::{
Editor, EditorEvent, EditorMode, MinimapVisibility, RestoreOnlyUnstagedDiffHunkDelegate,
SizingBehavior,
};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
ScrollHandle, TextStyleRefinement, WeakEntity, Window,
@ -682,7 +685,7 @@ fn create_editor_diff(
editor.set_show_code_actions(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_expand_all_diff_hunks(cx);
editor.set_render_diff_hunks_as_unstaged(true, cx);
editor.set_diff_hunk_delegate(Some(Arc::new(RestoreOnlyUnstagedDiffHunkDelegate)), cx);
editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
editor
})
@ -710,7 +713,6 @@ mod tests {
use agent_client_protocol::schema::v1 as acp;
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
use editor::RowInfo;
use feature_flags::{AcpBetaFeatureFlag, FeatureFlag as _, FeatureFlagAppExt as _};
use fs::FakeFs;
use gpui::{AppContext as _, TestAppContext};
use parking_lot::RwLock;
@ -848,11 +850,8 @@ mod tests {
}
#[gpui::test]
async fn test_hidden_elicitation_preserves_entry_index(cx: &mut TestAppContext) {
async fn test_elicitation_preserves_entry_index(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
cx.update_flags(false, vec![AcpBetaFeatureFlag::NAME.to_string()]);
});
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({})).await;
@ -897,7 +896,6 @@ mod tests {
)),
cx,
);
cx.update_flags(false, vec![]);
});
let view_state = cx.new(|_cx| {

View file

@ -5,7 +5,7 @@ use crate::{
completion_provider::{
AgentContextSelection, AvailableCommand, AvailableSkill, PromptCompletionProvider,
PromptCompletionProviderDelegate, PromptContextAction, PromptContextType,
SlashCommandCompletion,
PromptLocalCommand, SlashCommandCompletion,
},
mention_set::{Mention, MentionImage, MentionSet, insert_crease_for_mention},
};
@ -140,10 +140,13 @@ impl SessionCapabilities {
pub type SharedSessionCapabilities = Arc<RwLock<SessionCapabilities>>;
pub type SharedLocalCommands = Arc<RwLock<Vec<PromptLocalCommand>>>;
struct MessageEditorCompletionDelegate {
session_capabilities: SharedSessionCapabilities,
has_thread_store: bool,
message_editor: WeakEntity<MessageEditor>,
local_commands: SharedLocalCommands,
}
impl PromptCompletionProviderDelegate for MessageEditorCompletionDelegate {
@ -165,6 +168,18 @@ impl PromptCompletionProviderDelegate for MessageEditorCompletionDelegate {
self.session_capabilities.read().completion_skills()
}
fn available_local_commands(&self, _cx: &App) -> Vec<PromptLocalCommand> {
self.local_commands.read().clone()
}
fn run_local_command(&self, command: PromptLocalCommand, cx: &mut App) {
self.message_editor
.update(cx, |_this, cx| {
cx.emit(MessageEditorEvent::LocalCommandInvoked(command));
})
.ok();
}
fn slash_autocomplete_invoked(&self, cx: &mut App) {
// This may be called synchronously from inside a `MessageEditor`
// update (e.g. when pasting a slash command triggers completions),
@ -189,6 +204,7 @@ pub struct MessageEditor {
editor: Entity<Editor>,
workspace: WeakEntity<Workspace>,
session_capabilities: SharedSessionCapabilities,
local_commands: SharedLocalCommands,
agent_id: AgentId,
thread_store: Option<Entity<ThreadStore>>,
_subscriptions: Vec<Subscription>,
@ -213,6 +229,11 @@ pub enum MessageEditorEvent {
/// editor. Used by `ThreadView` to fire the global-skills scan
/// trigger; see `NativeAgent::ensure_skills_scan_started`.
SlashAutocompleteOpened,
/// Emitted when the user confirms a local slash command (scrolling,
/// exporting, feedback) in this editor's completion popup. `ThreadView`
/// handles it by running the corresponding action; see
/// `handle_message_editor_event`.
LocalCommandInvoked(PromptLocalCommand),
InputAttempted {
attempt: InputAttempt,
cursor_offset: usize,
@ -484,11 +505,13 @@ impl MessageEditor {
editor
});
let mention_set = cx.new(|_cx| MentionSet::new(project, thread_store.clone()));
let local_commands: SharedLocalCommands = Arc::new(RwLock::new(Vec::new()));
let completion_provider = Rc::new(PromptCompletionProvider::new(
MessageEditorCompletionDelegate {
session_capabilities: session_capabilities.clone(),
has_thread_store: thread_store.is_some(),
message_editor: cx.weak_entity(),
local_commands: local_commands.clone(),
},
editor.downgrade(),
mention_set.clone(),
@ -583,6 +606,7 @@ impl MessageEditor {
mention_set,
workspace,
session_capabilities,
local_commands,
agent_id,
thread_store,
_subscriptions: subscriptions,
@ -590,6 +614,10 @@ impl MessageEditor {
}
}
pub fn set_local_commands(&self, commands: Vec<PromptLocalCommand>) {
*self.local_commands.write() = commands;
}
pub fn set_session_capabilities(
&mut self,
session_capabilities: SharedSessionCapabilities,
@ -2211,8 +2239,9 @@ fn find_matching_bracket(text: &str, open: char, close: char) -> Option<usize> {
#[cfg(test)]
mod tests {
use std::{ops::Range, path::Path, path::PathBuf, sync::Arc};
use std::{ops::Range, path::Path, path::PathBuf, rc::Rc, sync::Arc};
use super::PromptLocalCommand;
use acp_thread::MentionUri;
use agent::{ThreadStore, outline};
use agent_client_protocol::schema::v1 as acp;
@ -2909,6 +2938,118 @@ mod tests {
});
}
/// Local commands set via `set_local_commands` must surface in the
/// slash-command popup, and confirming one must emit
/// `MessageEditorEvent::LocalCommandInvoked` (so `ThreadView` can run the
/// corresponding action) without leaving the `/keyword` in the editor.
#[gpui::test]
async fn test_local_commands_complete_and_emit_event(cx: &mut TestAppContext) {
init_test(cx);
let app_state = cx.update(AppState::test);
cx.update(|cx| {
editor::init(cx);
workspace::init(app_state.clone(), cx);
});
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
let window =
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let mut cx = VisualTestContext::from_window(window.into(), cx);
let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::from_acp_commands(
acp::PromptCapabilities::default(),
Vec::new(),
)));
let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
let message_editor = cx.new(|cx| {
MessageEditor::new(
workspace_handle,
project.downgrade(),
None,
session_capabilities.clone(),
"Test Agent".into(),
"Test",
EditorMode::AutoHeight {
max_lines: None,
min_lines: 1,
},
window,
cx,
)
});
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
true,
true,
None,
window,
cx,
);
});
message_editor.read(cx).focus_handle(cx).focus(window, cx);
let editor = message_editor.read(cx).editor().clone();
(message_editor, editor)
});
message_editor.read_with(&cx, |message_editor, _| {
message_editor.set_local_commands(vec![
PromptLocalCommand::ThumbsUp,
PromptLocalCommand::ThumbsDown,
]);
});
let invoked = Rc::new(std::cell::RefCell::new(Vec::new()));
let _subscription = cx.update(|_, cx| {
cx.subscribe(&message_editor, {
let invoked = invoked.clone();
move |_editor, event, _cx| {
if let MessageEditorEvent::LocalCommandInvoked(command) = event {
invoked.borrow_mut().push(*command);
}
}
})
});
// `/helpful` would fuzzy-match both commands ("helpful" is a
// subsequence of "not-helpful"), so drive the unambiguous keyword.
cx.simulate_input("/not-helpful");
cx.run_until_parked();
editor.read_with(&cx, |editor, _| {
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels(editor),
&[PromptLocalCommand::ThumbsDown.label().to_string()],
);
});
editor.update_in(&mut cx, |editor, window, cx| {
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
cx.run_until_parked();
editor.read_with(&cx, |editor, cx| {
// The `/keyword` text is removed when a local command is confirmed.
assert_eq!(editor.text(cx), "");
assert!(!editor.has_visible_completions_menu());
});
assert_eq!(
invoked.borrow().as_slice(),
&[PromptLocalCommand::ThumbsDown],
);
}
/// Opening slash-command autocomplete must emit
/// [`MessageEditorEvent::SlashAutocompleteOpened`]. `ThreadView`
/// subscribes to that event to fire the global-skills scan trigger

View file

@ -160,14 +160,14 @@ fn open_mention_uri(
workspace.update(cx, |workspace, cx| match mention_uri {
MentionUri::File { abs_path } => {
open_file(workspace, abs_path, None, window, cx);
open_abs_path_at_point(workspace, abs_path, None, window, cx);
}
MentionUri::Symbol {
abs_path,
line_range,
..
} => {
open_file(
open_abs_path_at_point(
workspace,
abs_path,
Some(Point::new(*line_range.start(), 0)),
@ -180,7 +180,7 @@ fn open_mention_uri(
line_range,
column,
} => {
open_file(
open_abs_path_at_point(
workspace,
abs_path,
Some(Point::new(*line_range.start(), column.unwrap_or(0))),
@ -339,41 +339,6 @@ fn open_skill_content_buffer(
workspace.add_item(pane, Box::new(editor), None, true, true, window, cx);
}
fn open_file(
workspace: &mut Workspace,
abs_path: PathBuf,
point: Option<Point>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
if let Some(point) = point {
if open_abs_path_at_point(workspace, abs_path.clone(), point, window, cx) {
return;
}
}
let project = workspace.project();
if let Some(project_path) =
project.update(cx, |project, cx| project.find_project_path(&abs_path, cx))
{
workspace
.open_path(project_path, None, true, window, cx)
.detach_and_log_err(cx);
} else if abs_path.exists() {
workspace
.open_abs_path(
abs_path,
OpenOptions {
focus: Some(true),
..Default::default()
},
window,
cx,
)
.detach_and_log_err(cx);
}
}
fn reveal_in_project_panel(
workspace: &mut Workspace,
abs_path: PathBuf,

Some files were not shown because too many files have changed in this diff Show more