Compare commits

..

222 Commits

Author SHA1 Message Date
Agus Zubiaga
bf3c5705e7 Checkpoint: Displaying debug info
Co-Authored-By: Bennet <bennet@zed.dev>
2025-09-22 18:47:03 -03:00
Bennet Bo Fenner
989ff500d9 Track edit events 2025-09-22 18:40:16 +02:00
Bennet Bo Fenner
a5ad9a9615 Port should_replace_prediction 2025-09-22 15:13:53 +02:00
Bennet Bo Fenner
ee4f8e7579 Port test_edit_prediction_basic_interpolation 2025-09-22 15:12:51 +02:00
Bennet Bo Fenner
4d9c4e187f Address check account todo 2025-09-22 14:57:13 +02:00
Bennet Bo Fenner
3944fb6ff7 Interpolate in suggest and refresh 2025-09-22 14:56:57 +02:00
Michael Sloan
49d9280344 Misc
Co-authored-by: Agus <agus@zed.dev>
2025-09-20 20:00:53 -06:00
Michael Sloan
1cc74ba885 Add ZED_ZETA2 env var 2025-09-19 16:02:05 -06:00
Michael Sloan
ad8bfbdf56 Send paths and ranges in zeta2 requests + add debug_info 2025-09-19 16:01:55 -06:00
Agus Zubiaga
439ab2575f Request completions
Co-Authored-By: Bennet <bennet@zed.dev>
2025-09-19 12:21:23 -03:00
Michael Sloan
a4024b495d Move cloud request building code to zeta2 + other misc changes 2025-09-19 00:56:14 -06:00
Michael Sloan
b511aa9274 Merge remote-tracking branch 'origin/zeta2-provider' into zeta2-cloud-request 2025-09-18 20:33:40 -06:00
Michael Sloan
cb0c4bec24 Progress preparing new cloud request + using index in excerpt selection
Co-authored-by: Agus <agus@zed.dev>
2025-09-18 17:39:23 -06:00
Jaeyong Sung
6b8ed5bf28 docs: Fix typo in Python configuration example (#38434)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-09-18 18:46:26 +00:00
Cole Miller
5fccde9b1b python: Install basedpyright if the basedpyright-langserver binary is missing (#38426)
Potential fix for #38377 

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <petertripp@gmail.com>
2025-09-18 18:45:02 +00:00
Bartosz Kaszubowski
c58763a526 git_ui: Reduce spacing between action icon and label (#38445)
# Why

Opinionated change: A bit uneven spacing between Git action icon and
label, in comparison to the border on the right in the segmented action
button was triggering my UI OCD a bit. 😅

# How

Remove the right margin from icon and icon + counter children of the
segmented Git action button in Git Panel. The default spacing from the
button layout seems to be enough to separate them from the left-side
label.

# Release Notes

- Reduced spacing between Git action icon and label in Git Panel

# Test plan

I have tested few cases, and made sure that the spacing is still
present, but icon (or icon and counter) does not feel too
separated/detached from the label.

### Before

<img width="384" height="186" alt="Screenshot 2025-09-18 at 20 11 16"
src="https://github.com/user-attachments/assets/8f353b8f-8e43-466d-88a9-567a82100b5f"
/>
<img width="384" height="186" alt="Screenshot 2025-09-18 at 20 13 19"
src="https://github.com/user-attachments/assets/1ecb4e1a-8a60-45b6-988e-966fb2b27ff5"
/>


### After

<img width="392" height="168" alt="Screenshot 2025-09-18 at 19 53 14"
src="https://github.com/user-attachments/assets/388d9b83-9906-4eac-82ed-13d2ae78c990"
/>
<img width="392" height="168" alt="Screenshot 2025-09-18 at 19 53 34"
src="https://github.com/user-attachments/assets/a179239b-ac09-479e-b688-f895ba75ca33"
/>
<img width="392" height="168" alt="Screenshot 2025-09-18 at 19 56 23"
src="https://github.com/user-attachments/assets/6ca10cf1-d46d-43b7-b847-832555823b8a"
/>
2025-09-18 18:34:13 +00:00
Agus Zubiaga
a6a2465954 edit prediction: Fix sub overflow in identifiers_in_range (#38438)
Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2025-09-18 18:28:41 +00:00
Cole Miller
439d31e2d4 Add branch rename action to Git panel (#38273)
Reopening #35136, cc @launay12u

Release Notes:

- git: added `git: rename branch` action to rename a branch (`git branch
-m`)

---------

Co-authored-by: Guillaume Launay <guillaume.launay@paylead.fr>
Co-authored-by: Peter Tripp <petertripp@gmail.com>
2025-09-18 18:17:13 +00:00
Agus Zubiaga
df50b5c14a edit prediction: Context debug view (#38435)
Adds a `dev: open edit prediction context` action that opens a new
workspace pane that displays the excerpts and snippets that would be
included in the edit prediction request.

Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
2025-09-18 15:09:44 -03:00
Anthony Eid
55d130a166 Fix chunks peek_with_bitmaps panic (#38430)
This panic only happened in debug builds because of a left shift
overflow. The slice range has bounds between 0 and 128. The 128 case
caused the overflow.

We now do an unbounded shift and a wrapped sub to get the correct
bitmask. If the slice range is 128 left, it should make 1 zero. Then the
wrapped sub would flip all bits, which is expected behavior.

Release Notes:

- N/A

Co-authored-by: Nia <nia@zed.dev>
2025-09-18 13:16:36 -04:00
Conrad Irwin
fcdab160f9 Settings refactor (#38367)
Co-Authored-By: Ben K <ben@zed.dev>
Co-Authored-By: Anthony <anthony@zed.dev>
Co-Authored-By: Mikayla <mikayla@zed.dev>

Release Notes:

- settings: Major internal changes to settings. The primary user-facing
effect is that some settings which did not make sense in project
settings files are no-longer read from there. (For example the inline
blame settings)

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Anthony <anthony@zed.dev>
2025-09-18 16:47:23 +00:00
Devdatta Talele
0a9023bce0 ui: Use hoverable tooltips for Badge component to fix tooltip behavior (#38387)
## Summary

Fixes #38362 - Privacy tooltip behavior issues in AI Setup onboarding

## Problem
The Privacy tooltip in AI Setup onboarding had incorrect behavior:
1. Tooltip remained visible after mouse left the Privacy button
2. Clicking the button didn't toggle tooltip properly
3. Clicking in intersection area between tooltip and button didn't work

## Root Cause
Badge component used `tooltip()` instead of `hoverable_tooltip()`,
causing:
- Immediate tooltip hiding when mouse left triggering element
- No support for tooltip content interaction
- Poor intersection area click handling

## Solution
**Single line change** in `crates/ui/src/components/badge.rs:61`:
```rust
// Before:
this.tooltip(move |window, cx| tooltip(window, cx))

// After:
this.hoverable_tooltip(move |window, cx| tooltip(window, cx))
```

## Technical Details
- Leverages existing GPUI `hoverable_tooltip()` infrastructure
- Enables 500ms grace period before tooltip hiding
- Allows hovering over tooltip content without disappearing
- Uses proper tooltip bounds detection for click handling
- Affects all Badge tooltips system-wide (positive improvement)
- Full backward compatibility - no API changes

## Test Plan
- [x] Hover over Privacy badge → tooltip appears
- [x] Move mouse away → tooltip stays visible for 500ms
- [x] Move mouse to tooltip content → tooltip remains visible
- [x] Click on tooltip content → properly handled
- [x] Move mouse completely away → tooltip hides after delay
- [x] Verify no regression in other Badge tooltip usage

Release Notes:

- N/A
2025-09-18 15:15:10 +00:00
Finn Evers
fb60f710e3 Make scrollbars auto-hide by default (#38340)
With this, scrollbars across the app will now auto-hide unless it is
specified that they should follow a specific setting.

Optimally, we would just track the user preference by default. However,
this is currently not possible. because the setting we would need to
read lives in `editor` and we cannot read that from within the `ui`
crate.

Release Notes:

- N/A
2025-09-18 12:08:14 -03:00
Danilo Leal
589e2c0fe4 agent: Make settings view more consistent across different sections (#38419)
Closes https://github.com/zed-industries/zed/issues/37660

This PR makes sections in the AI settings UI more consistent with each
other and also just overall simpler. One of the main changes here is
adding the tools from a given MCP server in a modal (as opposed to in a
disclosure within the settings view). That's mostly an artifact of
wanting to make all of the items within sections look more of the same.
Then, in the process of doing so, also changed the logic that we were
using to display MCP servers; previously, in the case of extension-based
servers, we were only showing those that were _configured_, which felt
wrong because you should be able to see everything you have _installed_,
despite of its status (configured or not).

However, there's still a bit of a bug (to be solved in a follow-up PR),
which already existed but it was just not visible given we'd only
display configured servers: an MCP server installed through an extension
stays as a "custom server" until it is configured. If you don't
configure it, you can't also uninstall it from the settings view (though
it is possible to do so via the extensions UI).

Release Notes:

- agent: Improve settings view UI and solve issue where MCP servers
would get unsorted upon turning them on and off (they're all
alphabetically sorted now).
2025-09-18 11:48:36 -03:00
Jakub Konka
21d8b19926 dap: Add more debug logs for child's stderr (#38418)
Without this, I would never have converged on @cole-miller's patch
https://github.com/zed-industries/zed/pull/38380 when debugging codelldb
not spawning in WSL!

Release Notes:

- N/A
2025-09-18 16:35:06 +02:00
Cole Miller
82686bf94c Start working on refreshing Python docs (#37880)
- Reflect that basedpyright is the new primary language server
- Discuss Ruff
- Deemphasize manual venv configuration for language servers

Release Notes:

- N/A

---------

Co-authored-by: Katie Geer <katie@zed.dev>
Co-authored-by: Piotr <piotr@zed.dev>
2025-09-18 08:53:30 -04:00
Michael Sloan
f562e7e157 edit predictions: Initial Tree-sitter context gathering (#38372)
Release Notes:

- N/A

Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Oleksiy <oleksiy@zed.dev>
Co-authored-by: Finn <finn@zed.dev>
2025-09-18 12:44:40 +00:00
Cole Miller
202dcb122f remote: Remove excess quoting in WSL build_command (#38380)
The built-up command for the WSL remote connection looks like

```
wsl.exe --distribution Ubuntu --user cole --cd /home/cole -- bash -c SCRIPT
```

Where `SCRIPT` is a command itself. We don't need extra quotes around
`SCRIPT` because we already pass it whole as a separate argument to
`wsl.exe`.

This isn't yet enough to get ACP servers working in WSL projects
(#38332), but it removes one roadblock.

Release Notes:

- windows: Fixed an issue that could prevent running binaries in WSL
remote projects.
2025-09-18 14:10:33 +02:00
Lukas Wirth
b1aa2723e9 editor: Reverse range of pending selection if required (#38410)
cc https://github.com/zed-industries/zed/issues/38129

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-18 10:58:10 +00:00
Lukas Wirth
59a609c9fc Partially revert "project: Fix terminal activation scripts failing on Windows for new shells (#37986) (#38406)
This partially reverts commit 4002602a89.
Specifically the parts that closes
https://github.com/zed-industries/zed/issues/38343

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-18 10:06:43 +00:00
Ben Brandt
ca05ff89f4 agent2: More efficent read file tool (#38407)
Before we were always reading the entire file into memory as a string.
Now we only read the range that is actually requested.

Release Notes:

- N/A
2025-09-18 10:05:05 +00:00
Lukas Wirth
9f9e8063fc workspace: Pop a toast if manually spawning a task fails (#38405)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-18 12:03:35 +02:00
Ben Brandt
32c868ff7d acp: Fix behavior of read_text_file for ACP agents (#38401)
We were incorrectly handling the line number as well as stripping out
line breaks when returning portions of files.

It also makes sure following is updated even when we load a snapshot
from cache, which wasn't the case before.

We also are able to load the text via a range in the snapshot, rather
than allocating a string for the entire file and then another after
iterating over lines in the file.

Release Notes:

- acp: Fix incorrect behavior when ACP agents requested to read portions
of files.
2025-09-18 09:38:59 +00:00
Romans Malinovskis
ed46e2ca77 helix: Apply modification (e.g. switch case) on a single character only in helix mode (#38119)
Closes #34192

Without selection, only current character would be affected.

Also if #38117 is merged too, then transformations in SelectMode behave
correctly too and selection is not collapsed.

Release Notes:

- helix: Implemented `~`, `` ` ``, `` Alt-` `` correctly in normal and
select modes

---------

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
2025-09-18 08:47:15 +00:00
Lukas Wirth
d85a6db6a3 git_ui: Use margin instead of padding for blame entries (#38397)
This makes the hover background change keep a visible border element
between the gutter and blame entries

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-18 08:22:26 +00:00
Miao
4b1e78cd5c terminal: Fix COLORTERM regression for true color support (#38379)
Closes #38304 

Release Notes:

- Fixed true color detection regression by setting `COLORTERM=truecolor`

---

Reason:

The regression is possibly introduced in [pr#36576: Inject venv
environment via the
toolchain](https://github.com/zed-industries/zed/pull/36576/files#diff-6f30387876b79f1de44f8193401d6c8fb49a2156479c4f2e32bc922ec5d54d76),
where `alacritty_terminal::tty::setup_env();` is removed.

The `alacritty_terminal::tty::setup_env();` does 2 things, which sets
`TERM` & `COLORTERM` envvar.
```rs
/// Setup environment variables.
pub fn setup_env() {
    // Default to 'alacritty' terminfo if it is available, otherwise
    // default to 'xterm-256color'. May be overridden by user's config
    // below.
    let terminfo = if terminfo_exists("alacritty") { "alacritty" } else { "xterm-256color" };
    unsafe { env::set_var("TERM", terminfo) };

    // Advertise 24-bit color support.
    unsafe { env::set_var("COLORTERM", "truecolor") };
}
```
2025-09-18 08:15:52 +00:00
Cole Miller
eaa1cb0ca3 acp: Add a basic test for ACP remoting (#38381)
Tests that the downstream project can see custom agents configured in
the remote server's settings, and that it constructs an appropriate
`AgentServerCommand`.

Release Notes:

- N/A
2025-09-18 00:02:44 -04:00
Cole Miller
ea473eea87 acp: Fix agent servers sometimes not being registered when Zed starts (#38330)
In local projects, initialize the list of agents in the agent server
store immediately. Previously we were initializing the list only after a
delay, in an attempt to avoid sending the `ExternalAgentsUpdated`
message to the downstream client (if any) before its handlers were
initialized. But we already have a separate codepath for that situation,
in the `AgentServerStore::shared`, and we can insert the delay in that
place instead.

Release Notes:

- acp: Fixed a bug where starting an external agent thread soon after
Zed starts up would show a "not registered" error.

---------

Co-authored-by: Michael <michael@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
2025-09-17 16:45:47 -04:00
Marshall Bowers
4912096599 collab: Remove unused feature flag queries (#38360)
This PR removes the feature flag queries, as they were no longer used.

Release Notes:

- N/A
2025-09-17 20:31:03 +00:00
Joseph T. Lyons
3c69144128 Update release URLs in release actions (#38361)
Release Notes:

- N/A
2025-09-17 20:22:50 +00:00
Jakub Konka
96111c6ef3 extension_host: Sanitize cwd path for ResolvedTask (#38357)
Ensures build task's CWD paths use POSIX-friendly path separator on
Windows host so that `std::path::Path` ops work as expected within the
Wasm guest.

Release Notes:

- N/A
2025-09-17 21:36:06 +02:00
Marshall Bowers
f6d08fe59c Remove /cargo-workspace slash command (#38354)
This PR removes the `/cargo-workspace` slash command.

We never fully shipped this—with it requiring explicit opt-in via a
setting—and it doesn't seem like the feature is needed in an agentic
world.

Release Notes:

- Removed the `/cargo-workspace` slash command.
2025-09-17 19:15:56 +00:00
Peter Tripp
86a2649944 docs: Add whitespace_map (#38355)
Adds docs for settings introduced in:
- https://github.com/zed-industries/zed/pull/37704

Release Notes:

- N/A
2025-09-17 15:04:18 -04:00
Finn Evers
f0b21508ec editor: Properly layout expand toggles with git blame enabled (#38349)
Release Notes:

- Fixed an issue where expand toggles were too large with the git blame
deployed.
2025-09-17 18:37:36 +00:00
localcc
3968b9cd09 Add open WSL shortcut (#38342)
Adds a shortcut to add a WSL distro for better wsl feature
discoverability.

- [x] Open wsl from open remote
- [x] Open local folder in wsl action
- [x] Open wsl shortcut (shortcuts to open remote)

Release Notes:

- N/A
2025-09-17 17:55:30 +00:00
Marshall Bowers
43f40c60fd rope: Fix spelling of peek_with_bitmaps (#38341)
This PR fixes the spelling of the `peek_with_bitmaps` method.

Release Notes:

- N/A
2025-09-17 17:02:13 +00:00
Joseph T. Lyons
824f695383 Rename Windows GitHub Issue template (#38339)
Release Notes:

- N/A
2025-09-17 19:25:50 +03:00
Nils Koch
50326ddc35 project_panel: Collapse top-level entries in Collapse all entries command (#38310)
Closes #11760

The command `project panel: collapse all entries` currently does not
collapse top-level entries (the workspaces themselves). I think this
should be expected behaviour if you only have a single workspace in your
project. However, if you have multiple workspaces, we should collapse
their top-level folders as well. This is the expected behaviour in the
screenshots in #11760.

For more context: Atm the `.retain` function empties the
`self.expanded_dir_ids` Hash Map, because the `expanded_entries` Vec is
(almost) never empty - it contains the id of the `root_entry` of the
workspace.


d48d6a7454/crates/project_panel/src/project_panel.rs (L1148-L1152)

We then update the `self.expanded_dir_ids` in the
`update_visible_entries` function, and since the Hash Map is empty, we
execute the `hash_map::Entry::Vacant` arm of the following match
statement.


d48d6a7454/crates/project_panel/src/project_panel.rs (L3062-L3073)

This change makes sure that we do not clear the `expanded_dir_ids`
HashMap and always keep the keys for all visible workspaces and
therefore we run the `hash_map::Entry::Occupied` arm, which does not
override the `expanded_dir_ids` anymore.



https://github.com/user-attachments/assets/b607523b-2ea2-4159-8edf-aed7bca05e3a

cc @MrSubidubi 

Release Notes:

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

---------

Co-authored-by: Finn Evers <finn.evers@outlook.de>
2025-09-17 17:58:46 +02:00
Ben Brandt
52521efc7b acp: update to v0.4 of Rust library (#38336)
Release Notes:

- N/A
2025-09-17 15:41:46 +00:00
localcc
4a7784cf67 Allow opening a local folder inside WSL (#38335)
This PR adds an option to allow opening local folders inside WSL
containers. (wsl_actions::OpenFolderInWsl). It is accessible via the
command palette and should be available to keybind.

- [x] Open wsl from open remote
- [x] Open local folder in wsl action
- [ ] Open wsl shortcut (shortcuts to open remote)

Release Notes:

- N/A
2025-09-17 15:39:47 +00:00
Lukas Wirth
f3b8c619e3 editor: Fix unwrap_syntax_node panicking by not setting selections (#38329)
Fixes ZED-11T

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-17 15:10:05 +00:00
Joseph T. Lyons
0f6dd84c98 Bump Zed to v0.206 (#38327)
Release Notes:

-N/A
2025-09-17 14:45:25 +00:00
Lukas Wirth
405a8eaf78 editor: Fix BlockMapWriter::blocks_intersecting_buffer_range creating invalid indexing ranges (#38325)
Fixes ZED-113
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-17 13:47:00 +00:00
itsaphel
c54e294965 Autosave files on close, when setting is afterDelay (#36929)
Closes https://github.com/zed-industries/zed/issues/12149
Closes #35524

Release Notes:

- Improved autosave behavior, to prevent a confirmation dialog when
quickly closing files and using the `afterDelay` setting

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
2025-09-17 13:08:29 +00:00
Smit Barmase
86834887da editor: Fix completions menu flashes on every keystroke in TSX files with emmet (#38320)
Closes https://github.com/zed-industries/zed/issues/37774

Bug in https://github.com/zed-industries/zed/pull/32927

Instead of using trigger characters to clear cached completions items,
now we check if the query is empty to clear it. Turns out Emmet defines
whole [alphanumeric as trigger
characters](279be10872/index.ts (L116))
which causes flickering.

Clear on trigger characters was introduced to get rid of cached
completions like in the case of "Parent.Foo.Bar", where "." is one of
the trigger characters. This works still since "." is not part of
`completion_query_characters` and hence we use it as a boundary while
building the current query. i.e in this case, the query would be empty
after typing ".", clearing cached completions.

Release Notes:

- Fixed issue where completions menu flashed on every keystroke in TSX
files with emmet extension installed.
2025-09-17 18:13:57 +05:30
Lukas Wirth
a5c29176a3 editor: Fix incorrect offset passed to acp completion provider (#38321)
Might fix | ZED-15G
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-17 12:02:39 +00:00
localcc
574b943081 Add wsl specific icon (#38316)
Release Notes:

- N/A
2025-09-17 10:47:09 +00:00
Lukas Wirth
399118f461 denoise: Fix LICENSE-GPL symlink (#38313)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-17 10:38:49 +00:00
Lukas Wirth
5ca3b998f3 fs: Do panic when failing to query modified timestamps (#38312)
Fixes ZED-1EW

Release Notes:

- N/A
2025-09-17 10:28:05 +00:00
Smit Barmase
d74b8bcf4c docs: Fix macOS development docs typo (#38311)
Release Notes:

- N/A
2025-09-17 15:46:53 +05:30
Lukas Wirth
28800c2a3b languages: Fix panic in python lsp adapters assuming settings shape (#38309)
Fixes ZED-1EV
Fixes ZED-S0
Fixes ZED-Q9

Release Notes:

- N/A
2025-09-17 10:09:15 +00:00
localcc
83d9f07547 Add WSL opening UI (#38260)
This PR adds an option to open WSL machines from the UI.

- [x] Open wsl from open remote
- [ ] Open local folder in wsl action
- [ ] Open wsl shortcut (shortcuts to open remote)

Release Notes:

- N/A
2025-09-17 09:44:16 +00:00
Lukas Wirth
c5ac1e6218 editor: Fix select_larget_syntax_node overflowing in multibuffers (#38308)
Fixes ZED-18Z

Release Notes:

- N/A
2025-09-17 11:36:20 +02:00
localcc
d48d6a7454 Fix empty nodes crash (#38259)
The crash occured because we raced against the platform windowing
backend to render a frame, and if we lost the race there would be no
frame on a window that we return, which breaks most of gpui

Release Notes:

- N/A
2025-09-17 11:26:53 +02:00
Lukas Wirth
a2de91827d agent_ui: Fix panic on editor changes in inline_assistant (#38303)
Fixes ZED-13P

Release Notes:

- N/A
2025-09-17 08:39:24 +00:00
Lukas Wirth
531f9ee236 Give most spawned threads names (#38302)
Release Notes:

- N/A
2025-09-17 10:11:51 +02:00
Michael Sloan
64d362cbce edit prediction: Initial implementation of Tree-sitter index (not yet used) (#38301)
Release Notes:

- N/A

---------

Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: oleksiy <oleksiy@zed.dev>
2025-09-17 07:25:14 +00:00
Kyrilasa
5d561aa494 agent_ui: Fix agent panel insertion to use cursor position (#38253)
Fix agent panel insertion to use cursor position

Closes #38216

Release Notes:
- Fixed agent panel text insertion to respect cursor position instead of
always appending to the end

## Before

[before.webm](https://github.com/user-attachments/assets/684d3cbe-4710-4724-8d2d-ac08f430dea8)

## After

[output.webm](https://github.com/user-attachments/assets/d1122d99-4efb-4a24-a408-db128814f98c)
2025-09-17 07:10:21 +00:00
Lukas Wirth
4ee2daeded markdown: Fix indented codeblocks having incorrect content ranges (#38225)
Closes https://github.com/zed-industries/zed/issues/37743

Release Notes:

- Fixed agent panel panicking when streaming indented codeblocks from
agent output
2025-09-17 06:48:47 +00:00
Cole Miller
c27d8e0c7a editor: Don't pull diagnostics on excerpts change in diagnostics editors (#38212)
This can lead to an infinite regress when using a language server that
supports pull diagnostics, since the excerpts for the diagnostics editor
are set based on the project's diagnostics.

Closes #36772

Release Notes:

- Fixed a bug that could cause duplicated diagnostics with some language
servers.
2025-09-16 21:58:24 -04:00
Marshall Bowers
f6c5c68751 collab: Remove user backfiller (#38291)
This PR removes the user backfiller from Collab.

Release Notes:

- N/A
2025-09-16 22:53:44 +00:00
Marshall Bowers
74e5b848ff cloud_llm_client: Make default_model and default_fast_model optional (#38288)
This PR makes the `default_model` and `default_fast_model` fields
optional on the `ListModelsResponse`.

Release Notes:

- N/A
2025-09-16 22:24:03 +00:00
Smit Barmase
ee399ebccf macOS: Make it easier to debug NSAutoFillHeuristicControllerEnabled (#38285)
Uses `setObject` instead of `registerDefaults`, so that it can be read
with `defaults read dev.zed.Zed`. Still can be overrided.

Release Notes:

- N/A
2025-09-17 03:49:47 +05:30
Max Brunsfeld
54c82f2732 Windows: Unminimize a window when activating it (#38287)
Closes #36287

Release Notes:

- Windows: Fixed an issue where a Zed window would stay minimized when
opening an existing file in that window via the Zed CLI.
2025-09-16 22:12:02 +00:00
Uwe Krause
e14a4ab90d Fix small spelling mistakes (#38284)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-16 21:58:40 +00:00
David Kleingeld
0343b5ff06 Add new crate denoise required by audio (#38217)
The audio crate will use the denoise crate to remove background noises
from microphone input.

We intent to contribute this to rodio. Before that can happen a PR needs
to land in candle. Until then this lives here.

Uses a candle fork which removes the dependency on `protoc` and has the PR's mentioned above already applied.

Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2025-09-16 21:49:26 +00:00
Marshall Bowers
26202e5af2 language_models: Use message field from Cloud error responses, if present (#38286)
This PR updates the Cloud language model provider to use the `message`
field from the Cloud error response, if it is present.

Previously we would always show the entire JSON payload in the error
message, but with this change we can show just the user-facing `message`
the error response is in a shape that we recognize.

Release Notes:

- N/A
2025-09-16 21:45:25 +00:00
George Waters
ee912366a3 Check if virtual environment is in worktree root (#37510)
The problem from issue #37509 comes from local virtual environments
created with certain approaches (including the 'simple' way of `python
-m venv`) not having a `.project` file with the path to the project's
root directory. When the toolchains are sorted, a virtual environment in
the project is not treated as being for that project and therefore is
not prioritized.

With this change, if a toolchain does not have a `project` associated
with it, we check to see if it is a virtual environment, and if it is we
use its parent directory as the `project`. This will make it the top
priority (i.e. the default) if there are no other virtual environments
for a project, which is what should be expected.

Closes #37509

Release Notes:

- Improved python toolchain prioritization of local virtual
environments.
2025-09-16 21:30:32 +02:00
David Kleingeld
673a98a277 Fix a number of spelling mistakes (#38281)
My pre push hooks keep failing on these. This is easier then disabling
and re-enabling those hooks all the time :)

Closes #ISSUE

Release Notes:

- N/A
2025-09-16 19:18:39 +00:00
VBB
5674445a61 Move keyboard shortcut for pane::GoForward (#38221)
Move keyboard shortcut for `pane:GoForward` so it's going to be
displayed as a shortcut hint in UI. Currently `Forward` is shown as a
hint, which isn't consistent with `GoBack` action and can be confusing.

Release Notes: 

- Improved the displayed keybinding for the `pane::GoForward` action on
Linux.
2025-09-16 18:33:55 +02:00
Jason Lee
53513cab23 Fix filled button hover background (#38235)
Release Notes:

- Fixed filled button hover background.

## Before


https://github.com/user-attachments/assets/fbc75890-d1a4-4a0c-b54e-ca2c7e63a661

## After


https://github.com/user-attachments/assets/a3595b01-e143-4cd0-8bc4-90db9ccfbf74


This appears to be a minor calculation error, not an intentional use of
this value.

If we pass `0.92` to `fade_out`, the calculated will be `alpha: 0.08`.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-09-16 14:47:10 +00:00
Smit Barmase
e885a939ba git_ui: Add tooltip for branch picker items (#38261)
Closes #38256

<img width="300" alt="image"
src="https://github.com/user-attachments/assets/5018951f-0f1b-4d5d-b59d-5b5266380e43"
/>


Release Notes:

- Added tooltip to Git branch picker items, making it easier to
distinguish long branch names.
2025-09-16 20:06:32 +05:30
Smit Barmase
a01a2ed0e0 languages: Add Tailwind CSS support for TypeScript (#38254)
Closes #37028

I noticed many projects use Tailwind in plain TypeScript (.ts) files, so
it makes sense to support them out of the box, alongside .js and .tsx
files we already handle. For example, see
[supabase](https://github.com/supabase/supabase/blob/master/packages/ui/src/lib/theme/defaultTheme.ts).

Note: You’ll still need to add `"classFunctions": ["cva", "cx"],`
manually for Tailwind completions to work in `cva` type methods. This is
because you don’t want completions on every string, only in specific
methods or regex matches. This is documented.

Release Notes:

- Added out-of-the-box support for Tailwind completions in `.ts` files.
2025-09-16 20:06:14 +05:30
Nathan Sobo
af3bc45a26 Drop ellipses from About Zed menu item (#38211)
Follow the macOS app style guideline.

Release Notes:

- N/A
2025-09-16 08:06:16 -06:00
Lukas Wirth
173074f248 search: Re-issue project search if search query is stale on replacement (#38251)
Closes https://github.com/zed-industries/zed/issues/34897

Release Notes:

- Fixed project search replacement replacing stale search results
2025-09-16 12:12:45 +00:00
Ben Brandt
a7cb64c64d Remove unused agent server settings module (#38250)
This was no longer in the module graph (the settings moved elsewhere) so
cleaning up the dead code.

Release Notes:

- N/A
2025-09-16 12:11:06 +00:00
Lukas Wirth
c6472fd7a8 agent_settings: Fix schema validation rejecting custom llm providers (#38248)
Closes https://github.com/zed-industries/zed/issues/37989

Release Notes:

- N/A
2025-09-16 10:23:49 +00:00
Ben Brandt
c0710fa8ca agent_servers: Set proxy env for all ACP agents (#38247)
- Use ProxySettings::proxy_url to read from settings or env 
- Export HTTP(S)_PROXY and NO_PROXY for agent CLIs 
- Add read_no_proxy_from_env and move parsing from main

Closes https://github.com/zed-industries/claude-code-acp/issues/46

Release Notes:

- acp: Pass proxy settings through to all ACP agents
2025-09-16 10:18:10 +00:00
Lukas Wirth
f321d02207 auto_update: Show update error on hover and open logs on click (#38241)
Release Notes:

- Improved error reporting when auto-updating fails
2025-09-16 08:07:02 +00:00
Lukas Wirth
1c09985fb3 worktree: Add more context to log_err calls (#38239)
Release Notes:

- N/A
2025-09-16 07:31:28 +00:00
Marshall Bowers
d986077592 client: Hide usage when not available (#38234)
Release Notes:

- N/A
2025-09-16 02:30:56 +00:00
Danilo Leal
555b6ee4e5 agent: Add small UI fixes (#38231)
Release Notes:

- N/A
2025-09-16 01:06:45 +00:00
Owen Kelly
6446963a0c agent: Make assistant panel input size configurable (#37975)
Release Notes:

- Added the `agent. message_editor_min_lines `setting to allow users to
customize the agent panel message editor default size by using a
different minimum number of lines.

<img width="800" height="1316" alt="Screenshot 2025-09-11 at 5 47 18 pm"
src="https://github.com/user-attachments/assets/20990b90-c4f9-4f5c-af59-76358642a273"
/>

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-09-16 00:27:25 +00:00
Finn Evers
ceb907e0dc onboarding: Add scrollbar to pages (#38093)
Closes #37214

This PR adds a scrollbar to the onboarding view and additionally ensures
the scroll state is properly reset when switching between the different
pages each time.

Release Notes:

- N/A
2025-09-15 19:55:02 -03:00
Alvaro Parker
3dbccc828e Fix hover element on ACP thread mode selector (#38204)
Closes #38197

This will render `^ click to also ...` on MacOS and `Ctrl + click to
also ...` on Windows and Linux.

|Before|After|
|-|-|
| <img width="683" height="197" alt="image"
src="https://github.com/user-attachments/assets/09909f1b-3163-40d1-b025-4eb9b159fbf3"
/> | <img width="683" height="197" alt="image"
src="https://github.com/user-attachments/assets/47d0290d-afa2-4b1b-a588-adfe3130d0b1"
/>|

On Mac: 

<img width="683" height="197" alt="image"
src="https://github.com/user-attachments/assets/f63103b5-1ceb-4193-ae6c-be55b97106e0"
/>

Release Notes:

- Fixed keymap hint when hovering over mode selector

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-09-15 22:48:04 +00:00
Michael Sloan
853e625259 edit predictions: Add new excerpt logic (not yet used) (#38226)
Release Notes:

- N/A

---------

Co-authored-by: agus <agus@zed.dev>
2025-09-15 16:29:58 -06:00
Kenny
0784bb8192 docs: Add "Copy as Markdown" button to toolbar (#38218)
## Summary
Adds a "Copy as Markdown" button to the documentation toolbar that
allows users to easily copy the raw markdown content of any
documentation page.

This feature is inspired by similar implementations on sites like
[Better Auth docs](https://www.better-auth.com/docs/installation) and
[Cloudflare Workers docs](https://developers.cloudflare.com/workers/)
which provide easy ways for users to copy documentation content.

## Features
- **Button placement**: Positioned between theme toggle and search icon
for optimal UX
- **Content fetching**: Retrieves raw markdown from GitHub's API for the
current page
- **Consistent styling**: Matches existing toolbar button patterns

## Test plan
- [x] Copy functionality works on all documentation pages
- [x] Toast notifications appear and disappear correctly
- [x] Button icon animations work properly (spinner → checkmark → copy)
- [x] Styling matches other toolbar buttons
- [x] Works in both light and dark themes

## Screenshots
The button appears as a copy icon between the theme and search buttons
in the left toolbar.
<img width="798" height="295" alt="image"
src="https://github.com/user-attachments/assets/37d41258-d71b-40f8-b8fe-16eaa46b8d7f"
/>
<img width="1628" height="358" alt="image"
src="https://github.com/user-attachments/assets/fc45bc04-a290-4a07-8d1a-a010a92be033"
/>

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-09-15 21:57:23 +00:00
Mikayla Maki
9046091164 Add a test that would have caught the bug last week (#38222)
This adds a test to make sure that the default value of the auto update
setting is always true. We manually re-applied the broken code from last
week, and confirmed that this test fails with that code.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-15 18:10:28 +00:00
Danilo Leal
6384966ab5 agent: Improve some items in the settings view UI (#38199)
All described in each commit; mostly small things, simplifying/clearing
up the UI.

Release Notes:

- N/A
2025-09-15 13:35:39 -03:00
Ben Kunkle
8b9c74726a docs: Call out Omarchy specifically in regards to issues with amdvlk (#38214)
Closes #28851


Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-15 16:03:45 +00:00
Kaan Kuscu
63586ff2e4 Add new injections for Go (#37605)
support for injecting sql, json, yaml, xml, html, css, js, lua and csv
value

if you use `/* lang */` before string literals, highlights them

**Example:**

```go
const sqlQuery = /* sql */ "SELECT * FROM users;" // highlights as SQL code
```

<img width="629" height="46" alt="Screenshot 2025-09-05 at 06 17 49"
src="https://github.com/user-attachments/assets/80f404d8-0a47-428d-bdb5-09fbee502cfe"
/>


Closes #ISSUE

Release Notes:

- Go: Added support for injecting sql, json, yaml, xml, html, css, js, lua and csv language highlights into string literals, when they are prefixed with `/* lang */`

**Example:**

```go
const sqlQuery = /* sql */ "SELECT * FROM users;" // Will be highlighted as SQL code
```
2025-09-15 15:51:03 +00:00
Conrad Irwin
35e5aa4e71 Re-add VSCode syntax node motions (#38208)
Closes #ISSUE

Release Notes:

- (preview only) restored ctrl-shift-{left,right} for Larger/Smaller
syntax node. This is VSCode's default and avoids the breaking change
from #37874
2025-09-15 09:18:07 -06:00
Agus Zubiaga
d8dd2b2977 Add zeta2 to registry 2025-09-15 12:17:01 -03:00
Richard Feldman
7ea94a32be Create failed tool call entries for missing tools (#38207)
Release Notes:

- When an agent requests a tool that doesn't exist, this is now treated
as a failed tool call instead of stopping the thread.
2025-09-15 15:07:14 +00:00
Agus Zubiaga
e19995431b Create zeta2 crate 2025-09-15 10:46:10 -03:00
Piotr Osiewicz
6d6c3d648a lsp: Fix overnotifying about open buffers for unrelated servers (#38196)
Do not report all open buffers to new instances of the same language
server, as they can respond with ~spurious errors.

This regressed in  https://github.com/zed-industries/zed/pull/34142

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

Release Notes:

- Fixed Zed overly notifying language servers about open buffers, which
could've resulted in confusing errors in multi-language projects (in
e.g. Go).
2025-09-15 15:20:04 +02:00
Hichem
53b2f37452 Enhance layout and styling of tool list in AgentConfiguration (#38195)
Improve the layout and styling of the tool list in the
AgentConfiguration, ensuring better responsiveness and visual clarity.

closes #38194

<img width="1270" height="738" alt="image"
src="https://github.com/user-attachments/assets/86345e57-4fd0-43b8-8b8d-6209dc635dfb"
/>

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-09-15 13:00:22 +00:00
Lukas Wirth
92b946e8e5 acp_thread: Properly use project terminal API (#38186)
Closes https://github.com/zed-industries/zed/issues/35603

Release Notes:

- Fixed shell selection for terminal tool
2025-09-15 12:43:41 +00:00
Hakan Ensari
e9b4f59e0f Fix external agent authentication with spaces in paths (#38175)
This fixes terminal-based authentication for external ACP agents (Claude
Code, Gemini CLI) when file paths contain spaces, like "Application
Support" on macOS and "Program Files" on Windows.

When users click authentication buttons or type `/login`, they get
errors like `Cannot find module '/Users/username/Library/Application'`
because the path gets split at the space.

The fix removes redundant `shlex::try_quote` calls from
`spawn_external_agent_login`. These were causing double-quoting since
the terminal spawning code already handles proper shell escaping.

Added a test to verify paths with spaces aren't pre-quoted.

Release Notes:

- Fixed external agent authentication failures when file paths contain
spaces

---------

Co-authored-by: Hakan Ensari <hakanensari@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
2025-09-15 10:20:27 +00:00
Finn Evers
989adde57b Add scrollbars to markdown preview and syntax tree view (#38183)
Closes https://github.com/zed-industries/zed/issues/38141

This PR adds default scrollbars to the markdown preview and syntax tree
view.

Release Notes:

- Added scrollbars to the markdown preview and syntax tree view.
2025-09-15 10:17:27 +00:00
Lukas Wirth
393d6787a3 terminal: Do not auto close shell terminals if they error out (#38182)
Closes https://github.com/zed-industries/zed/issues/38134

This also reduces an annoying level of shell nesting

Release Notes:

- N/A
2025-09-15 10:09:25 +00:00
Finn Evers
4a582504d4 ui: Follow-up improvements to the scrollbar component (#38178)
This PR lands some more improvements to the reworked scrollbars.

Namely, we will now explicitly paint a background in cases where a track
is requested for the specific scrollbar, which prevents a flicker, and
also reserve space only if space actually needs to be reserved. The
latter was a regression introduced by the recent changes.

Release Notes:

- N/A
2025-09-15 09:53:33 +00:00
Smit Barmase
cfb2925169 macOS: Disable NSAutoFillHeuristicController on macOS 26 (#38179)
Closes #33182

From
https://github.com/zed-industries/zed/issues/33182#issuecomment-3289846957,
thanks @mitchellh.

Release Notes:

- Fixed an issue where scrolling could sometimes feel choppy on macOS
26.
2025-09-15 15:17:27 +05:30
Lukas Wirth
14f4e867aa terminal: Do not auto close shell terminals if they error out (#38180)
cc https://github.com/zed-industries/zed/issues/38134
Release Notes:

- N/A
2025-09-15 09:43:05 +00:00
Ben Brandt
4d54ccf494 agent_servers: Let Gemini CLI know it is running in Zed (#38058)
By passing through Zed as the surface, Gemini can know which editor it
is running in.

Release Notes:

- N/A
2025-09-15 08:30:46 +00:00
Tim Vermeulen
5b1c87b6a6 Fix incorrect ANSI color contrast adjustment on some background colors (#38155)
The `Hsla` -> `Rgba` conversion sometimes results in negative (but very
close to 0) color components due to floating point imprecision, causing
the `.powf(constants.main_trc)` computations in the `srgb_to_y` function
to evaluate to `NaN`. This propagates to `apca_contrast` which then
makes `ensure_minimum_contrast` unconditionally return `black` for
certain background colors. This PR addresses this by clamping the rgba
components in `impl From<Hsla> for Rgba` to 0-1.

Before/after:
<img width="1044" height="48" alt="before"
src="https://github.com/user-attachments/assets/771f809f-3959-43e9-8ed0-152ff284cef8"
/>
<img width="1044" height="49" alt="after"
src="https://github.com/user-attachments/assets/5fd6ae25-1ef0-4334-90d1-7fc5acf48958"
/>

Release Notes:

- Fixed an issue where ANSI colors were incorrectly adjusted to improve
contrast on some background colors
2025-09-15 07:52:56 +00:00
Vladimir Varankin
0fef17baa2 Hide BasedPyright banner in toolbar when dismissed (#38135)
This PR fixes the `BasedPyrightBanner`, making sure the banner is
completely hidden in the toolbar, when it was dismissed, or it's not
installed.

Without the fix, the banner still occupies some space in the toolbar,
making the UI looks inconsistent when editing a Python file. The bug is
**especially prominent** when the toolbar is hidden in the user's
settings (see below).

_Banner is shown_
<img width="1470" height="254" alt="Screenshot 2025-09-14 at 11 36 37"
src="https://github.com/user-attachments/assets/1415b075-0660-41ed-8069-c2318ac3a7cf"
/>

_Banner dismissed_
<img width="1470" height="207" alt="Screenshot 2025-09-14 at 11 36 44"
src="https://github.com/user-attachments/assets/828a3fba-5c50-4aba-832c-3e0cc6ed464b"
/>

_Banner dismissed (and the toolbar is hidden)_
<img width="1470" height="177" alt="Screenshot 2025-09-14 at 12 07 25"
src="https://github.com/user-attachments/assets/41aa5861-87df-491f-ac7e-09fc1558dd84"
/>

Closes n/a

Release Notes:

- Fixed the basedpyright onboarding banner
2025-09-15 09:43:04 +02:00
Umesh Yadav
526196917b language_models: Add support for API key to Ollama provider (#34110)
Closes https://github.com/zed-industries/zed/issues/19491

Release Notes:

- Ollama: Added configuration of URL and API key for remote Ollama provider.

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Oliver Azevedo Barnes <oliver@liquidvoting.io>
Co-authored-by: Michael Sloan <michael@zed.dev>
2025-09-15 06:34:26 +00:00
Michael Sloan
a598fbaa73 ai: Show "API key configured for {URL}" for non-default urls (#38170)
Followup to #38163, also makes some changes intended to be included in
that PR.

Release Notes:

- N/A
2025-09-15 05:49:25 +00:00
Michael Sloan
634ae72cad Misc cleanup + clear language model provider API key editors when API keys are submitted (#38165)
Followup to #38163 along with some other misc cleanups

Release Notes:

- N/A
2025-09-15 05:08:38 +00:00
Michael Sloan
98edf1bf0b Reload API keys when URLs configured for LLM providers change (#38163)
Three motivations for this:

* Changing provider URL could cause credentials for the prior URL to be
sent to the new URL.
* The UI is in a misleading state after URL change - it shows a
configured API key, but on restart it will show no API key.
* #34110 will add support for both URL and key configuration for Ollama.
This is the first provider to have UI for setting the URL, and this
makes these issues show up more directly as odd UI interactions.

#37610 implemented something similar for the OpenAI and OpenAI
compatible providers. This extracts out some shared code, uses it in all
relevant providers, and adds more safety around key use.

I haven't tested all providers, but the per-provider changes were pretty
mechanical, so hopefully work properly.

Release Notes:

- Fixed handling of changes to LLM provider URL in settings to also load
the associated API key.
2025-09-15 03:36:24 +00:00
Ben Kunkle
1090c47a90 Move Keymap Editor Table component to UI crate (#38157)
Closes #ISSUE

Move the data table component created for the Keymap Editor to the UI
crate. Additionally includes simplifications to the scrollbar component
in UI necessary for the table component to support scrollbar
configurations, and a fix for an issue with the table component where
when used with the `.row` API instead of `uniform_list` the rows would
render on top of each other.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-14 15:53:07 -04:00
Michael Sloan
be7b22b0dc Show docs for all documented actions in keymap.json (#38156)
Release Notes:

- Fixed a bug where action documentation while editing `keymap.json` was
only shown for actions that take input.
2025-09-14 19:38:48 +00:00
Michael Sloan
f3e49e1b05 x11: Don't skip consecutive same key press events in the same batch (#38154)
This has noticeable misbehavior when framerates are low (in my case this
sometimes happens when CPUs are throttled and compilation is happening),
as now a batch of x11 events can contain events over the span of 100s of
millis. So in that case, key press repetitions with quite normal typing
are skipped.

Under normal operating conditions it can be reproduced by running this
and quickly switching to Zed:

> sleep 1; for i in {1..5}; do xdotool type --delay 5 "aaaaaa "; xdotool
key Return; done

Output before looks like:
```
aaa
aaaaa
aaa
aaa
aaaa
```

Output after looks like:
```
aaaaaa
aaaaaa
aaaaaa
aaaaaa
aaaaaa
```

This behavior was added in #13955.

Release Notes:

- N/A
2025-09-14 19:09:15 +00:00
Alvaro Parker
0adc6ddaad ui: Fix scrollbar showing despite being disabled by tracked setting (#38152)
Closes #38147 

The scrollbar's `show_state` field was always being initialized to
`VisibilityState::Visible`, ignoring the `show_setting` value.

Release Notes:

- N/A
2025-09-14 18:39:23 +00:00
Ben Kunkle
99b71677c6 Ability to update JSON arrays (#38087)
Closes #ISSUE

Adds the ability to our JSON updating code to update arrays within other
objects. Previously updating of arrays was limited to just top level
arrays (i.e. `keymap.json`) however this PR makes it so nested arrays
are supported as well using `#{index}` syntax as a key.

This PR also fixes an issue with the array updating code that meant that
updating empty json values `""` or an empty `keymap.json` file in the
case of the Keymap Editor would fail instead of creating a new array.

Release Notes:

- Fixed an issue where keybindings would fail to save in the Keymap
Editor if the `keymap.json` file was completely empty
2025-09-14 12:36:26 -04:00
Alexander
1c27a6dbc2 Do not escape glob pattern in dynamic Jest/Vitest test names (#36999)
Related to #35090

Release Notes:

- javascript: Fixed name escaping in dynamic jest/vitest task names

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-09-14 18:05:03 +02:00
Martin Pool
256a91019a ci: Move doctests to a separate parallel job (#38111)
Follow on from #37851 

This may reduce CI time by running doctests in parallel with other
tests. It also makes it easier to find the results.

Example output:
https://github.com/zed-industries/zed/actions/runs/17698218116/job/50300398669?pr=38111

At least on this run, the doctests finished before the main Linux tests,
which makes sense because there are many fewer doctests. So they should
not be on the critical path.

Thanks @maxdeviant for the prompt.

<img width="615" height="513" alt="image"
src="https://github.com/user-attachments/assets/bcafa636-a68c-4602-97f4-61f7904e6a7b"
/>


Release Notes:

- N/A
2025-09-14 12:01:00 -04:00
Jakub Konka
85aa458b9c helix: Drop back to normal mode after yanking in select mode (#38133)
Follow-up to https://github.com/zed-industries/zed/pull/38117.
@romaninsh I'd appreciate if you could have a look :-)

Release Notes:

- N/A
2025-09-14 15:49:56 +00:00
Piotr Osiewicz
37239fd66b Use serde 1.0.221 instead of serde_derive hackery (#38137)
serde 1.0.221 introduced serde_core into the build graph, which should
render explicitly depending on serde_derive for faster build times an
obsolote method.

Besides, I'm not even sure if that worked for us. My hunch is that at
least one of our deps would have `serde` with derive feature enabled..
and then, most of the crates using `serde_derive` explicitly were also
depending on gpui, which depended on `serde`.. thus, we wouldn't have
gained anything from explicit dep on `serde_derive`

Release Notes:

- N/A
2025-09-14 14:01:04 +02:00
Smit Barmase
2b1f7d5763 project_panel: Fix primary and secondary click on blank area (#38139)
Follow up https://github.com/zed-industries/zed/pull/38008

Release Notes:

- N/A
2025-09-14 17:28:51 +05:30
Romans Malinovskis
813a9bb0bc Fix select in Helix mode (#38117)
Hotfixes issue I have introduced in #37748.

Without this, helix mode select not working at all in `main` branch.

Release Notes:

- N/A
2025-09-14 10:32:12 +02:00
Marshall Bowers
e40a950bc4 collab: Add orb_portal_url column to billing_customers table (#38124)
This PR adds an `orb_portal_url` column to the `billing_customers`
table.

Release Notes:

- N/A
2025-09-14 02:42:39 +00:00
Marshall Bowers
89e527c23b Fix typo in default settings (#38123)
This PR fixes a typo in the default settings file.

Release Notes:

- N/A
2025-09-14 02:39:34 +00:00
Casper van Elteren
c50b561e1c Expose REPL Settings (#37927)
Closes #37829

This PR introduces and exposes `REPLSettings` to control the number of
lines and columns in the REPL. These settings are integrated into the
existing configuration system, allowing for customization and management
through the standard settings interface.

#### Changes
- Added `REPLSettings` struct with `max_number_of_lines` and
`max_number_of_columns` fields.
- Integrated `REPLSettings` with the settings system by implementing the
`Settings` trait.
- Ensured compatibility with the workspace and existing settings
infrastructure.

Release Notes:

- Add configuration "repl" to settings to configure max lines and
columns for repl.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-09-13 23:17:44 +00:00
Ben Gubler
13113ab311 Add setting to show/hide title bar (#37428)
Closes #5120

Release Notes:

- Added settings for hiding and showing title bar



https://github.com/user-attachments/assets/aaed52d0-6278-4544-8932-c6bab531512a
2025-09-13 22:54:00 +00:00
Umesh Yadav
01f181339f language_models: Remove unnecessary LM Studio connection refused log (#37277)
In zed logs you can see these logs of lmstudio connection refused.
Currently zed connects to lmstudio by default as there is no credential
mechanism to check if the user has enabled lmstudio previously or not
like we do with other providers using api keys.

This pr removes the below annoying log and makes the zed logs less
polluted.

```
2025-09-01T02:11:33+05:30 ERROR [language_models] Other(error sending request for url (http://localhost:1234/api/v0/models)

Caused by:
    0: client error (Connect)
    1: tcp connect error: Connection refused (os error 61)
    2: Connection refused (os error 61))
```

Release Notes:

- N/A

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-09-13 20:17:13 +02:00
Marshall Bowers
d046016ef5 collab: Add Orb cancellation date to billing_subscriptions table (#38098)
This PR adds an `orb_cancellation_date` column to the
`billing_subscriptions` table.

Release Notes:

- N/A
2025-09-13 04:13:04 +00:00
Michael Sloan
e43ad858d8 Add debug methods for visually annotating ranges (#38097)
This allows you to write `buffer_snapshot.debug(ranges, value)` and it
will be displayed in the buffer (or multibuffer!) until that callsite
runs again. `ranges` can be any position (`usize`, `Anchor`, etc), any
range, or a slice or vec of those. `value` just needs a `Debug` impl.
These are stored in a mutable global for convenience, and this is only
available in debug builds.

For example, using this to visualize the captures of the brackets
Tree-sitter query:

<img width="1215" height="480" alt="image"
src="https://github.com/user-attachments/assets/c1878fc7-f6b3-4e27-949e-ecf67a7906b9"
/>

Release Notes:

- N/A
2025-09-13 03:37:24 +00:00
Finn Evers
ded6467604 Refactor the scrollbar component (#36105)
Closes https://github.com/zed-industries/zed/issues/37621
Improves https://github.com/zed-industries/zed/issues/24623

Adding scrollbars withing Zed's UI currently is rather cumbersome, as it
requires the copying of a lot of code in order for these to work. Wiring
up settings for scrollbar visibilty always has to be done at the call
site and the state has to be saved and maintained by the caller as well.
Similarly, reserving space has to also be handled by the caller.

This PR changes the way scrollbars work in Zed fundamentally by making
use of the new `use_keyed_state` APIs: Instead of saving the state at
the call site, the window now keeps track of the state corresponding to
scrollbars. This enables us to add scrollbars with e.g. one simple call
on divs:
```rust
div()
    .vertical_scrollbar(window, cx)
```
will add a scrollbar to the corresponding container. There are some more
improvements regarding tracking of scrollbar visibility settings (which
is now handled by a trait for each setting that supports this) as well
as reserving space.
Additionally, all needed stuff for layouting, catching events and
reserving space is also now managed by the scrollbar component instead.
This drastically reduces the amount of event listeners and makes
layouting of two scrollbars easier.

Furthermore, this paves the way for more improvements to scrollbars,
such as graceful auto-hide. Only downsight here is that we lose some
customizability in a few areas. However, once this lands, we gain the
ability to quickly follow these up without breaking stuff elsewhere.

This also already fixes a few bugs:
- Scrollbars no longer flicker on first render. 
- Auto-hide now properly works for all scrollbars.
- If the content size changes, the scrollbar is updated on the same
frame. Both of these happened because we were computing the scrollbar
sizes too early, causing us to use the sizes from the previous frame or
unitialized sizes.
- The project panel no longer jumps if scrolled all the way to the
bottom and the scrollbar actually auto-hides.

Still TODO:
- [x] Fix scrolling in the debugger memory view
- [x] Clean up some more in the scrollbar component and reduce clones
there
- [x] Ensure we don't over-notify the entity the scrollbar is rendered
within
- [x] Make sure auto-hide properly works for all cases
- [x] Check whether we want to implement the scrollbar trait for
`UniformList`s as well
    - ~~ [ ] Use for uniformlist where possible~~ Postponed
- [x] Improve layout for cases where we render both scrollbars.

Release Notes:

- N/A
2025-09-13 00:43:16 +02:00
Finn Evers
53c5db4495 Highlight Zed log file by default if log language is available (#38091)
This ensures that we highlight the log file with the log extension
should the extension be installed.

If it is not installed, we just fallback to the default of no
highlighting, but also log no errors.

Release Notes:

- N/A
2025-09-12 23:39:15 +02:00
Michael Sloan
cd2ecbbd27 Add logging of missing or unexpected capture names in Tree-sitter queries (#37830)
Now logs warnings for unrecognized capture names and logs errors for
missing required captures.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2025-09-12 15:01:16 -06:00
Finn Evers
e71012a2f8 Automatically uninstall release extension prior to dev extension install (#38088)
Closes https://github.com/zed-industries/zed/issues/31106

This fixes an issue where you would have to manually uninstall the
release extension before installing the dev extension in case that is
locally installed.

Release Notes:

- Installing a dev extension will now automatically remove the release
extension should it be installed.
2025-09-12 22:48:24 +02:00
Martin Pool
b9cf5886e4 Run doctests in CI and fix up existing doctests (#37851)
Follows on from
https://github.com/zed-industries/zed/pull/37716#pullrequestreview-3195695110
by @SomeoneToIgnore

After this the doctests will be run in CI to check that the examples are
still accurate.

Note that doctests aren't run by Nextest: you can run them locally with
`cargo test --doc`.

Summary:
* Run tests from CI
* Loosen an exact float comparison to match approximately (otherwise it
fails)
* Fixed one actual bug in the tests for `dilate` where the test code
assumed that `dilate` mutates `self` rather than returning a new object
* Add some `must_use` on some functions that seemed at risk of similar
bugs, following the Rust stdlib style to add it where ignoring the
result is almost certainly a bug.
* Fix some cases where the doc examples seem to have gone out of date
with the code
* Add imports to doctests that need them
* Add some dev-dependencies to make the tests build
* Fix the `key_dispatch` module docstring, which was accidentally
attached to objects within that module
* Skip some doctest examples that seem like they need an async
environment or that just looked hard to get running

AI usage: I asked Claude to do some of the repetitive tests. I checked
the output and fixed up some things that seemed to not be in the right
spirit of the test, or too longwinded.

I think we could reasonably run the tests on only Linux to save CI
CPU-seconds and latency, but I haven't done that yet, partly because of
how it's implemented in the action.

Release Notes:

- N/A
2025-09-12 23:24:04 +03:00
Ben Kunkle
174a0b1517 Fix line indicator format setting (#38071)
Closes #ISSUE

Release Notes:

- Fixed an issue where the `line_indicator_format` setting would not
update based on the value in `settings.json`
2025-09-12 15:55:19 -04:00
Anthony Eid
e4b754a19f settings ui: Fix dropdown menu rendering the same entries for different settings (#38083)
Using `window.use_state` made the element IDs match between elements,
thus causing the same menu to be shared for drop down menus. I switched
to `window.use_keyed_state` and used a value's path as it's element id

Release Notes:

- N/A
2025-09-12 15:03:24 -04:00
Jacob
5f20b905a5 Add support for named folder icons (#36351)
Adds a `named_directory_icons` field to the icon theme that can be used
to specify a collection of icons for collapsed and expanded folders
based on the folder name.

The `named_directory_icons` is a map from the folder name to a
`DirectoryIcons` object containing the paths to the expanded and
collapsed icons for that folder:

```json
{
  "named_directory_icons": {
    ".angular": {
      "collapsed": "./icons/folder_angular.svg",
      "expanded": "./icons/folder_angular_open.svg"
    }
  }
}

```

Closes #20295

Also referenced
https://github.com/zed-industries/zed/pull/23987#issuecomment-2638869213

Example using https://github.com/jacobtread/zed-vscode-icons/ which I've
ported over from a VSCode theme,

<img width="609" height="1307" alt="image"
src="https://github.com/user-attachments/assets/2d3c120a-b2f0-43fd-889d-641ad4bb9cee"
/>

Release Notes:

- Added support for icon themes to change the folder icon based on the
directory name.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-09-12 14:55:25 -04:00
Karl-Erik Enkelmann
4c758bd0b7 fix command name for hover in docs (#38084)
Release Notes:

- N/A
2025-09-12 18:46:48 +00:00
Alvaro Parker
4b7595c94c git: Add git stash picker (#35927)
Closes #ISSUE

This PR continues work from #32821 by adding a stash entry picker for
pop/drop operations. Additionally, the stash pop action in the git panel
is now disabled when no stash entries exist, preventing error logs from
attempted pops on empty stashes.

Preview:

<img width="1920" height="1256" alt="Screenshot From 2025-09-11
14-08-31"
src="https://github.com/user-attachments/assets/b2f32974-8c69-4e50-8951-24ab2cf93c12"
/>

<img width="1920" height="1256" alt="Screenshot From 2025-09-11
14-08-12"
src="https://github.com/user-attachments/assets/992ce237-43c9-456e-979c-c2e2149d633e"
/>



Release Notes:

- Added a stash picker to pop and drop a specific stash entry
- Disabled the stash pop action on the git panel when no stash entries
exist
- Added git stash apply command
- Added git stash drop command
2025-09-12 14:45:38 -04:00
Finn Evers
2143c59fba svg_preview: Ensure preview properly updates in follow mode (#38081)
This fixes an issue where we would not update neither the path nor the
editor that was listened to during follow mode, which in turn would
cause the preview to become stale.

Fix here is to update the subscription whenever the active item changes
and also update the associated path accordingly.


Release Notes:

- Fixed an issue where the SVG preview would not update when following
the active editor.
2025-09-12 18:08:21 +00:00
Danilo Leal
2b3ca360c3 Fix flicker in short context menus that have documentation aside (#38074)
Menu items in the context menu component have the ability to display a
documentation aside popover. However, because this docs aside popover
was setup as a sibling flex container to the actual menu popover, if the
menu had a short amount of items and the docs popover is bigger than the
menu, this flickering would happen, making it essentially unusable:


https://github.com/user-attachments/assets/74956254-fff6-4c5c-9f79-02998c64a105

So, this PR makes the docs aside popover in wide window sizes
absolutely-positioned relative to the menu container, which removes all
flickering. On top of that, I'm adding a `DocumentationEdge` enum that
allows to control the edge anchor of the docs aside, which is useful in
this particular mode selector example to make the layout work well.


https://github.com/user-attachments/assets/a3e811e1-86b4-4839-a219-c3b0734532b3

When the window is small, the docs aside continue to be a sibling flex
container, which causes a super subtle shift in the items within the
menu popover. This is something I want to pursue fixing, but didn't want
to delay this PR too much.

Release Notes:

- N/A
2025-09-12 14:22:35 -03:00
Jakub Konka
85f7bb6277 extension_host: Replace backslashes with forward slashes for cwd on Windows (#38072)
Instead of passing CWD verbatim from the Windows host with backslashes
and all, we now rewrite it into a more POSIX-happy format featuring
forward slashes which means `std::path::Path` operations now work within
WASI with Windows-style paths.

Release Notes:

- N/A
2025-09-12 19:22:24 +02:00
Smit Barmase
7377a898e8 project_panel: Allow dragging folded directories onto other items (#38070)
In https://github.com/zed-industries/zed/pull/22983 we made it possible
to drag items onto folded directories.

This PR handles the reverse: dragging folded directories onto other
items.

Release Notes:

- Improved drag-and-drop support by allowing folded directories to be
dragged onto other items in Project Panel.
2025-09-12 22:28:43 +05:30
Marshall Bowers
8ebe812c24 Format CONTRIBUTING.md (#38073)
This PR formats `CONTRIBUTING.md` using Prettier.

Release Notes:

- N/A
2025-09-12 16:39:54 +00:00
Conrad Irwin
7f1c7c1910 Update CONTRIBUTING to reflect reality (#38016)
Release Notes:

- N/A
2025-09-12 10:21:02 -06:00
bemyak
503284db45 Update oo7 to 0.5.0 (#38043)
Resolves Incorrect Secret error in Secret Service integration

Closes #34024 

Release Notes:

- Fixed Secret Service integration sometimes producing `Incorrect
secret` error
2025-09-12 10:18:44 -06:00
Bennet Bo Fenner
2aa564eeb7 Remove ACP feature flags (#38055)
This removes the `gemini-and-native` and `claude-code` feature flags.
Also, I removed a bunch of unused agent1 code that we do not need
anymore.

Initially I wanted to remove much more of the `agent` code, but noticed
some things that we need to figure out first:
- The inline assistant + context strip use `Thread`/`ContextStore`
directly
- We need some replacement for `ToolWorkingSet`, so we can access
available tools (as well as context server tools) in other places, e.g.
the agent configuration and the configure profile modal

Release Notes:

- N/A
2025-09-12 18:07:59 +02:00
Romans Malinovskis
cba9ff55c7 Helix Select Mode (#37748)
Please credit @eliaperantoni, for the original PR (#34136).
Merge after (#34060) to avoid conflicts.

Closes https://github.com/zed-industries/zed/issues/33838
Closes https://github.com/zed-industries/zed/issues/33906

Release Notes:
- Helix will no longer sometimes fall out into "normal" mode, will
remain in "helix normal" (example: vv)
- Added dedicated "helix select" mode that can be targeted by
keybindings

Known issues:
- [ ] Helix motion, especially surround-add will not properly work in
visual mode, as it won't call `helix_move_cursor`. It is possible
however to respect self.mode in change_selection now.
- [ ] Some operations, such as `Ctrl+A` (increment) or `>` (indent) will
collapse selection also. I haven't found a way to avoid it.

---------

Co-authored-by: fantacell <ghub@giggo.de>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-12 17:47:07 +02:00
Agus Zubiaga
a577128163 Update acp to 0.2.1 (#38068)
Release Notes:

- N/A
2025-09-12 15:22:51 +00:00
David Kleingeld
687c2c88c7 Fix experimental audio volume being significantly too low (#38062)
The rodio channelcount convertor halves the volume. This addresses that
in most cases by using the default channelcount for the system
microphone which is usually 2.

A proper fix will follow later as part of the de-noising PR

Release Notes:

- N/A
2025-09-12 13:56:48 +00:00
Lukas Wirth
2a03b6b80c terminal: Fix test_basic_terminal test (#38059)
`echo` isn't a program on windows, so we need to spawn a shell that
executes it for the test

Release Notes:

- N/A
2025-09-12 13:13:23 +00:00
Lukas Wirth
e68aa18fd4 project: Fix task arguments being quoted incorrectly for nushell and powershell (#38056)
Release Notes:

- Fixed task arguments being quoted incorrectly for nushell and
powershell

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
2025-09-12 12:02:39 +00:00
Lukas Wirth
592b013013 language: Split LSP installation handling into a separate trait (#38046)
Part of reworking our installation handling to allow for multiple
different versions to be handled

Release Notes:

- Fixed pre-release lsp fetching setting not having an affect until
restarting Zed
2025-09-12 09:37:45 +00:00
Umesh Yadav
1142408675 language_models: Add provider options for OpenRouter models (#37979)
Supersedes: #34500

Also this will allow to fix this: #35386 without the UX changes but
providers can now be control through settings as well within zed.

Just rebased the latest main and docs added. Added @AurelienTollard as
co-author as it was started by him everything else remains the same from
original PR.

Release Notes:

- Added ability to control Provider Routing for OpenRouter models from
settings.

Co-authored-by: Aurelien Tollard <tollard.aurelien1999@gmail.com>
2025-09-12 11:17:55 +02:00
Alvaro Parker
8201f3d72f Use \x00 representation instead of literal null characters (#38033)
When working on the git stash picker PR (#35927) I notice that my test
was detected as a binary file on the git diff view and on GitHub. This
was due to the fact that I was using the literal char \0 (instead of a
proper representation like `\x00` or `\u{0000}`) character in my test
strings. This causes problems with git diff and GitHub's diff viewer,
and a reviewer might even assume that the file is corrupted, not
viewable or even malicious.

Looking at the rest of the codebase, only at `crates/git/src/commit.rs`
this character was used, so I replaced it with `\x00` which is a more
common representation of the null character in Rust strings.

It can also be seen that the PR that introduced this code, can't be
viewed properly on Github:
https://github.com/zed-industries/zed/pull/27636/files#diff-31114f0b22306b467482573446f71c638277510b442a10e60dd9a8667ccd93c3

Closes #ISSUE

Release Notes:

- Use `\x00` representation instead of literal null character in strings
to improve compatibility with git diff and GitHub's diff viewer.

Since the file is not viewable from the "Files changed" tab on Github,
this is the changed code:


dcd743aca4/crates/git/src/commit.rs (L66-L74)
2025-09-11 23:29:20 -06:00
Conrad Irwin
fcfc54c515 Allow SplitAndMove on panes (#38034)
Updates #19350

Release Notes:

- Add `pane::SplitAndMove{Up,Down,Left,Right}` to allow creating a split
without cloning the current buffer.
2025-09-12 03:18:28 +00:00
Julia Ryan
ffb85d7e81 Update crash handling docs (#38026)
Also removed the symbolicate script, which we could replace with a
`minidump-stackwalk` wrapper that downloaded sources/unstripped binaries
from github releases if that's helpful for folks.

Release Notes:

- N/A
2025-09-12 03:00:35 +00:00
Richard Feldman
405d7d7476 Don't send contents of large @mention-ed files (#38032)
<img width="598" height="311" alt="Screenshot 2025-09-11 at 9 39 12 PM"
src="https://github.com/user-attachments/assets/b526e648-37cf-4412-83a0-42037b9fc94d"
/>

This is for both ACP and the regular agent. Previously we would always
include the whole file, which can easily blow the context window on huge
files.

Release Notes:

- When `@mention`ing large files, the Agent Panel now send an outline of
the file instead of the whole thing.
2025-09-11 22:18:42 -04:00
Joseph T. Lyons
bdf44e55aa Prevent Discord URL preview in good first issue notifications (#38030)
Release Notes:

- N/A
2025-09-12 01:16:51 +00:00
Cole Miller
45ee1327a4 Add handling of git's core.excludesFile (#33592)
Taking over from #28314.

Part of https://github.com/zed-industries/zed/issues/4824

Co-authored-by: Paul Nameless <reacsdas@gmail.com>

Release Notes:

- Zed now respects git's `core.excludesFile` (~/.config/git/ignore) in
addition to .gitignore.

---------

Co-authored-by: Paul Nameless <reacsdas@gmail.com>
2025-09-11 21:00:03 -04:00
Anthony Eid
b60e705782 Fix auto update not defaulting to true (#38022)
#37337 Made `AutoUpdateSetting` `FileContent =
AutoUpdateSettingsContent` which caused a deserialization bug to occur
because the field it was wrapping wasn't optional. Thus serde would
deserialize the wrapped type `bool` to its default value `false`
stopping the settings load function from reading the correct default
value from `default.json`

I also added a log message that states when the auto updater struct is
checking for updates to make this easier to test.

Release Notes:

- fix auto update defaulting to false
2025-09-11 22:29:06 +00:00
Joseph T. Lyons
2bb50acb58 Add action to send good first issues to discord (#38021)
Release Notes:

- N/A
2025-09-11 21:54:33 +00:00
Piotr Osiewicz
87f5e72fc0 python: Add built-in support for Ty (#37580)
- **Rename PythonLSPAdapter to PyrightLspAdapter**
- **ah damn**
- **Ah damn x2**

Release Notes:

- Python: Added built-in support for [ty](https://docs.astral.sh/ty/)
language server (disabled by default).

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>
Co-authored-by: Zsolt Dollenstein <zsol.zsol@gmail.com>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-09-11 21:52:05 +00:00
Michael Sloan
11b7913956 Refactor/optimize tree-sitter utilities for finding nodes enclosing ranges (#37943)
#35053 split out these utility functions. I found the names / doc
comments a bit confusing so this improves that. Before that PR there was
also a mild inefficiency - it would walk the cursor all the way down to
a leaf and then back up to an ancestor.

Release Notes:

- N/A
2025-09-11 21:42:09 +00:00
Anthony Eid
ff2eebf522 settings ui: Add basic support for drop down menus (#38019)
Enums with six or less fields can still use toggle groups by adding a
definition.

I also renamed the `OpenSettingsEditor` action to `OpenSettingsUi`

Release Notes:

- N/A
2025-09-11 21:16:16 +00:00
Max Brunsfeld
c4d75ea6d5 Windows: Fix issues with paths in extensions (#37811)
### Background

Zed extensions use WASI to access the file-system. They only have
read-write access to one specific folder called their work dir. But
extensions do need to be able to *refer* to other arbitrary files on the
user's machine. For instance, extensions need to be able to look up
existing binaries on the user's `PATH`, and request that Zed invoke them
as language servers. Similarly, extensions can create paths to files in
the user's project, and use them as arguments in commands that Zed
should run. For these reasons, we pass *real* paths back and forth
between the host and extensions; we don't try to abstract over the
file-system with some virtualization scheme.

On Windows, this results in a bit of mismatch, because `wasi-libc` uses
*unix-like* path conventions (and thus, so does the Rust standard
library when compiling to WASI).

### Change 1 - Fixing `current_dir`

In order to keep the extension API minimal, extensions use the standard
library function`env::current_dir()` to query the location of their
"work" directory. Previously, when initializing extensions, we used the
`env::set_current_dir` function to set their work directory, but on
Windows, where absolute paths typically begin with a drive letter, like
`C:`, the [`wasi-libc` implementation of
`chdir`](d1793637d8/libc-bottom-half/sources/chdir.c (L21))
was prepending an extra forward slash to the path, which caused
`current_dir()` to return an invalid path.

See https://github.com/bytecodealliance/wasmtime/issues/10415

In this PR, I've switched our extension initialization function to
*bypass* wasi-libc's `chdir` function, and instead write directly to
wasi-libc's private, internal state. This is a bit of a hack, but it
causes the `current_dir()` function to do what we want on Windows
without any changes to extensions' source code.

### Change 2 - Working around WASI's relative path handling

Once `current_dir` was fixed (giving us correct absolute paths on
Windows), @kubkon and I discovered that without the spurious leading `/`
character, windows absolute paths were no longer accepted by Rust's
`std::fs` APIs, because they were now recognized as relative paths, and
were being appended to the working directory.

We first tried to override the `__wasilibc_find_abspath` function in
`wasi-libc` to make it recognize windows absolute paths as being
absolute, but that functionality is difficult to override. Eventually
@kubkon realized that we could prevent WASI-libc's CWD handling from
being linked into the WASM file by overriding the `chdir` function.
wasi-libc is designed so that if you don't use their `chdir` function,
then all paths will be interpreted as relative to `/`. This makes
absolute paths behave correctly. Then, in order to make *relative* paths
work again, we simply add a preopen for `.`. Relative paths will match
that.

### Next Steps

This is a change to `zed-extension-api`, so we do need to update every
Zed extension to use the new version, in order for them to work on
windows.

Release Notes:

- N/A

---------

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
2025-09-11 13:56:06 -07:00
Cole Miller
d5d30b5c44 python: Add built-in support for Ruff (#37804)
Release Notes:

- python: The Ruff native language server is now available without
installing an extension.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-09-11 20:52:07 +00:00
Michael Sloan
7655e22ff5 Fix panics from unicode slicing in license detection (#38015)
Closes #37954

Release Notes:

- N/A
2025-09-11 19:57:24 +00:00
Joseph T. Lyons
7a83a7fbd0 Fix congratsbot (#38013)
We need a PAT to have permission to check team information. Also, the
COAUTHOR_TEMPLATES didn't feel quite right. Skipping this for now.

Release Notes:

- N/A
2025-09-11 19:30:37 +00:00
Piotr Osiewicz
3cb3f01406 languages: Pass fs into the init function (#38007)
Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-09-11 19:00:51 +00:00
Martin Pool
46aa05e240 Fix regex syntax in matching os-release (#38010)
From
https://github.com/zed-industries/zed/pull/37712#issuecomment-3281970712,
thank you @zywo

Release Notes:

- N/A
2025-09-11 21:22:55 +03:00
Julia Ryan
a33af4e9c0 Remove legacy panic handling (#37947)
@maxdeviant We can eventually turn down the panic telemetry endpoint,
but should probably leave it up while there's still a bunch of stable
users hitting it.

@maxbrunsfeld We're optimistic that this change also fixed the macos
crashed-thread misreporting. We think it was because the
`CrashContext::exception` was getting set to `None` only on macos, while
on linux it was getting a real exception value from the sigtrap. Now
we've unified and it uses `SIGABRT` on both platforms (I need to double
check that this works as expected for windows).

We unconditionally set `RUST_BACKTRACE=1` for the current process so
that we see backtraces when running in a terminal by default. This
should be fine but I just wanted to note it since it's a bit abnormal.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-11 11:06:04 -07:00
Smit Barmase
116c6549f6 project_panel: Make rest of the project panel drag and drop target (#38008)
Closes #25854

You can now drag-and-drop on the remaining space in the project panel to
drop entries/external paths in the last worktree.


https://github.com/user-attachments/assets/a7e14518-6065-4b0f-ba2c-823c70f154f4

Release Notes:

- Added support for drag-and-drop files and external paths into the
empty space of the project panel, placing them in the last folder you
have added to the project.
2025-09-11 23:25:10 +05:30
Joseph T. Lyons
da8c7a1256 Polish congratsbot (#38005)
Release Notes:

- N/A
2025-09-11 12:04:09 -04:00
Joseph T. Lyons
2b04186b0f Only run congratsbot for non-staff (#38000)
Release Notes:

- N/A
2025-09-11 15:07:11 +00:00
Lukas Wirth
462293667b editor: Re-use multibuffers when opening the same locations again (#37994)
A very primitive attempt, we just key the editor with the locations and
re-use the editor if we open a new buffer with the same initial
locations and title.

Release Notes:

- Added reusing of reference search buffers when applicable
2025-09-11 16:58:11 +02:00
David Kleingeld
e5c0373011 Make rodio audio input compile under windows (#37999)
Follow up on https://github.com/zed-industries/zed/pull/37786

adds conditional cmp removing use of libwebrtc on windows/freebsd

They cant compile livekit yet. This removes microphone and echo
cancellation on those platforms however they can not join calls due to
the same cause so it does not matter.

Documentation and error handing improvements

Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
2025-09-11 14:45:42 +00:00
Kirill Bulatov
a066794e8d Document two task rerun modes better (#37996)
Part of https://github.com/zed-industries/zed/issues/37720

Release Notes:

- N/A
2025-09-11 14:41:02 +00:00
Joseph T. Lyons
f6b6d4a9fe Add congratsbot (#37998)
Release Notes:

- N/A
2025-09-11 14:29:44 +00:00
Joseph T. Lyons
238dab4a9c Adjust release notes Discord webhook name (#37997)
Release Notes:

- N/A
2025-09-11 14:22:24 +00:00
Kirill Bulatov
d1c6c9d035 Remove old LSP definitions (#37995)
Last time MultiLspQuery was used in Zed was 0.201.x and Nightly is of
0.205.x version, hence it's time to clean up the old code.

Release Notes:

- N/A
2025-09-11 14:07:04 +00:00
Finn Evers
d7f3d08c59 editor: Ensure placeholder text wraps properly after font size change (#37992)
Follow-up of https://github.com/zed-industries/zed/pull/37919

This fixes an issue where the placeholder text in editors would not wrap
properly in cases where the font size was changed.

Before:


https://github.com/user-attachments/assets/479c919f-5815-4164-b46d-75f31b5dc56f

After:


https://github.com/user-attachments/assets/9f63ab9f-eac2-4f3e-864c-2b96b58f2d71


Release Notes:

- N/A
2025-09-11 13:20:05 +00:00
tidely
4db19a3a96 search: Fix buffer search history navigation (#37924)
Closes #36109 

Adds an additional option to `search` and `update_matches` to specify
whether the update should affect the search history.

Release Notes:

- Fix navigating buffer search history
2025-09-11 14:55:14 +02:00
Smit Barmase
c4e8fe1fb7 theme: Ensure opaque for overlay fallback (#37987)
Closes #37965

Release Notes:

- N/A
2025-09-11 18:04:40 +05:30
Lukas Wirth
4002602a89 project: Fix terminal activation scripts failing on Windows for new shells (#37986)
Tasks are still disabled as there seem to be more issues with it

Release Notes:

- N/A
2025-09-11 12:16:08 +00:00
ImFeH2
6ae83b4740 Support indent regex with inline comments in Python (#37903)
Closes #36491

This issue is caused by the Python language configuration treating
compound statements (such as for loops and if statements) that end with
an inline comment as not requiring an increased indent.

Release Notes:

- python: Correctly indent lines starting the blocks (for, finally, if,
else, try) that have trailing comments.
2025-09-11 13:39:39 +02:00
Finn Evers
eec6bfebbb extension_host: Fix operation status whilst installing dev extension (#37985)
This fixes a minor issue where we would show "Removing extension ..." in
the status bar when we would actually be installing it.

Release Notes:

- Fixed an issue where installing a dev extension would show the
installation status as "removing" in the activity indicator.
2025-09-11 10:58:24 +00:00
Finn Evers
9875969cba editor: Allow no context for the excerpt_context_lines setting (#37982)
Closes #37980

There seems to be no reason to hard limit this to 1, and we even have
existing UX for this case already:

<img width="1530" height="748" alt="Bildschirmfoto 2025-09-11 um 11 22
57"
src="https://github.com/user-attachments/assets/d6498318-c905-4d3c-90ab-60e4f2bb6c48"
/>

(Notice the different arrows in the gutter area for single lines)

Hence, allowing the value to honor the request from the issue

Release Notes:

- Allowed `0` as a value for the `excerpt_context_lines` setting
2025-09-11 09:42:18 +00:00
David Matter
59502289e7 Document Tailwind CSS language server configuration (#37970)
Added configuration instructions for Tailwind CSS language server.

Release Notes:

- N/A
2025-09-11 07:16:36 +00:00
Mitch (a.k.a Voz)
f764077020 Change keymap precedence to favor user (#37557)
Closes #35623 

Previously if a base keymap had a `null` set to an action, leading to a
`NoAction` being assigned to the keymap, if a user wanted to take
advantage of that keymap (in this particular case, `cmd-2`), the keymap
binding check would favor the `NoAction` over the user, since
technically the context depth matched better. Instead, we should always
prefer the user's settings over whatever base or default.

Release Notes:

- Fixed keymap precedence by favoring user settings over base keymap /
configs.
2025-09-11 00:29:31 -06:00
Marshall Bowers
9708c8d507 feature_flags: Move feature flag definitions to their own module (#37956)
This PR moves the feature flag definitions to their own module so that
they aren't intermingled with the feature flag infrastructure itself.

Release Notes:

- N/A
2025-09-11 02:43:19 +00:00
Marshall Bowers
f205732074 feature_flags: Remove unused llm-closed-beta feature flag (#37955)
This PR removes the `llm-closed-beta` feature flag, as it is no longer
used.

Release Notes:

- N/A
2025-09-11 02:23:18 +00:00
Ryan Hawkins
aee21ca17f Allow for commit amends with no file changes (#37256)
This will users to change the wording of the most recent commit,
something they might want to do if they realize they made a small typo
of some kind or if the formatting of their commit message is wrong, but
don't have any other changes they need to make.

Release Notes:

- Commit messages can now be amended in the UI without any other changes
needing to be made.

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-09-11 00:20:32 +00:00
Danilo Leal
816c4817d0 Fix code actions menu item font size (#37951)
Follow up to https://github.com/zed-industries/zed/pull/37824, which
made items be cut-off in the _editor_ instance of the code actions menu.
This PR applies the default UI font size for the code action menu items
only when the origin is the quick actions bar.

Release Notes:

- Fix code actions menu items being cut-off in the editor.
2025-09-10 20:16:51 -03:00
Dima
0f9232a10d Fix wrong cursor shape description in Configuring Zed page (#37933)
It was previously copied incorrectly from `Terminal: Copy On Select`.

Release Notes:

- Fixed wrong description in `Terminal: Cursor Shape` in `Configuring
Zed` document
2025-09-10 20:05:23 -03:00
Marshall Bowers
db367cc6bf scheduler: Add missing constructs for Cloud (#37948)
This PR adds some missing constructs that are needed by Cloud to the
scheduler.

Release Notes:

- N/A
2025-09-10 22:02:23 +00:00
Ben Kunkle
2ce0641fe0 settings_ui: Handle enums with fields (#37945)
Closes #ISSUE

Adds handling for Enums with fields (i.e. not `enum Foo { Yes, No }`) in
Settings UI. Accomplished by creating default values for each element
with fields (in the derive macro), and rendering a toggle button group
with a button for each variant where switching the active variant sets
the value in the settings JSON to the default for the new active
variant.

Release Notes:

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

---------

Co-authored-by: Conrad <conrad@zed.dev>
2025-09-10 18:02:08 -04:00
David Kleingeld
95ccce3095 Rodio audio (#37786)
Adds input to the experimental rodio_audio pipeline.

Enable with:
```json
"audio": {
  "experimental.rodio_audio": true
}
```

Additionally enables automatic volume 
control for incoming audio:
```json
"audio": {
  "experimental.control_output_volume": true
}
```

Release Notes:

- N/A
2025-09-10 22:48:33 +02:00
Julia Ryan
14de161d06 Compress minidumps (#37797)
@notpeter this should fix that issue you were seeing where a generated
minidump was too big to upload with the sentry api.

Release Notes:

- N/A
2025-09-10 13:22:54 -07:00
Anthony Eid
b8c30f448f Improve Tab Map performance (#32243)
## Context

While looking into: #32051 and #16120 with instruments, I noticed that
`TabSnapshot::to_tab_point` and `TabSnapshot::to_fold_point` are a
common bottleneck between the two issues. This PR takes the first steps
into closing the stated issues by improving the performance of both
those functions.

### Method

`to_tab_point` and `to_fold_point` iterate through each character in
their rows to find tab characters and translate those characters into
their respective transformations. This PR changes this iteration to take
advantage of the tab character bitmap in the `Rope` data structure and
goes directly to each tab character when iterating.

The tab bitmap is now passed from each layer in-between the `Rope` to
the `TabMap`.

### Testing 

I added several randomized tests to ensure that the new `to_tab_point`
and `to_fold_point` functions have the same behavior as the old methods
they're replacing. I also added `test_random_chunk_bitmap` on each layer
the tab bitmap is passed up to the `TabMap` to make sure that the bitmap
being passed is transformed correctly between the layers of
`DisplayMap`.

`test_random_chunk_bitmap` was added to these layers:
- buffer
- multi buffer
- custom_highlights
- inlay_map
- fold_map

## Benchmarking 

I setup benchmarks with criterion that is runnable via `cargo bench -p
editor --profile=release-fast`. When benchmarking I had my laptop
plugged in and did so from the terminal with a minimal amount of
processes running. I'm also on a m4 max

### Results 

#### To Tab Point

Went from completing 6.8M iterations in 5s with an average time of
`736.13 ns` to `683.38 ns` which is a `-7.1875%` improvement

#### To Fold Point

Went from completing 6.8M iterations in 5s with an average time of
`736.55 ns` to `682.40 ns` which is a `-7.1659%` improvement

#### Editor render 

Went from having an average render time of `62.561 µs` to `57.216 µs`
which is a `-8.8248%` improvement

#### Build Buffer with one long line

Went from having an average buffer build time of `3.2549 ms` to `3.2635
ms` which is a `+0.2151%` regression within the margin of error

#### Editor with 1000 multi cursor input 

Went from having an average edit time of `133.05 ms` to `122.96 ms`
which is a `-7.5776%` improvement

Release Notes:

- N/A

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-09-10 16:13:41 -04:00
Marshall Bowers
cb75c2aeb7 Make plans backwards compatible (#37941)
This PR fixes the backwards compatibility of the new `Plan` variants.

We can't add new variants to the wire representation, as old clients
won't be able to understand them.

Release Notes:

- N/A
2025-09-10 20:11:07 +00:00
Joseph T. Lyons
2c29eac29f Fetch all staff in get-preview-channel-changes script (#37940)
Release Notes:

- N/A
2025-09-10 20:05:51 +00:00
AidanV
a94b0931c7 editor: Fix cuts on end of line cutting whole line (#34553)
Closes #19816

Release Notes:

- Improved `ctrl-k` (`editor::CutToEndOfLine`) behavior when used at the
end of lines
- Add option to make `editor::CutToEndOfLine` not gobble newlines.
   ```json
   {
     "context": "Editor",
"bindings": { "ctrl-k": ["editor::CutToEndOfLine", { "stop_at_newlines":
true }] }
   },
   ```

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-09-10 14:40:00 -04:00
Michael Sloan
441a934d84 Remove unnecessary Option from functions querying buffer outline (#37935)
`None` case wasn't being used

Release Notes:

- N/A
2025-09-10 18:21:51 +00:00
Ilija Tovilo
b28c979aae language_settings: Add whitespace_map setting (#37704)
This setting controls which visible characters are used to render
whitespace when the show_whitespace setting is enabled.

Release Notes:

- Added `whitespace_map` setting to control which visible characters are
used to render whitespace when the `show_whitespace` setting is enabled.

---------

Co-authored-by: Nia Espera <nia@zed.dev>
2025-09-10 18:19:24 +00:00
Smit Barmase
22e31a0d41 Fix crash when filtering items in Picker (#37929)
Closes #37617

We're already using `get` in a bunch of places, this PR updates the
remaining spots to follow the same pattern. Note that the `ix` we read
in `render_match` can sometimes be stale.

The likely reason is that we run the match-update logic asynchronously
(see
[here](138117e0b1/crates/picker/src/picker.rs (L643))).
That means it's possible to render items after the list's [data
update](138117e0b1/crates/picker/src/picker.rs (L652))
but before the [list
reset](138117e0b1/crates/picker/src/picker.rs (L662)),
in which case the `ix` can be greater than that of our updated data.

Release Notes:

- Fixed crash when filtering MCP tools.
2025-09-10 23:06:09 +05:30
Piotr Osiewicz
c0b583c9ef keymap_editor: Move OpenKeymapEditor action into zed_actions (#37928)
This lets us remove title_bar's dependency on keymap_editor, which in
turns improves dev build times by ~0.5s for me

Release Notes:

- N/A
2025-09-10 16:57:05 +00:00
Finn Evers
6441099a67 gpui: Fix blending of colors in HighlightStyle::highlight (#37666)
Whilst looking into adding support for RainbowBrackes, we stumbled upon
this: Whereas for all properties during this blending, we take the value
of `other` if it is set, for the color we actually take `self.color`
instead of `other.color` if `self.color` is at full opacity.
`Hsla::blend` returns the latter color if it is at full opacity, which
seems wrong for this case. Hence, this PR swaps these.

Will not merge before the next release, to ensure that we don't break
something somewhere unexpected.

Release Notes:

- N/A
2025-09-10 18:56:49 +02:00
Andy Brauninger
611b96627b Fix typo in development docs for Windows and Linux (#37925)
Fixes same typo ("collabortation") as #37607 but for the Windows and
Linux dev docs.

Release Notes:

- N/A
2025-09-10 19:43:50 +03:00
Bennet Bo Fenner
630340d659 agent: Fix mention completion sometimes not dismissing on space (#37922)
Previously we would still show a completion menu even when the user
typed an unrecognised mode with an argument,
e.g. `@something word`.
This PR ensures that we only show the completion menu, when the part
after the `@` is a known mode (e.g. `file`/`symbol`/`rule`/...)

Release Notes:

- Fix an issue where completions for `@mentions` in the agent panel
would sometimes not be dismissed when typing a space
2025-09-10 15:58:04 +00:00
Bennet Bo Fenner
acb3406eb8 editor: Wrap placeholder if text overflows (#37919)
This fixes an issue where long placeholders would be cut off, e.g. in a
Claude Code thread:

<img width="387" height="115" alt="image"
src="https://github.com/user-attachments/assets/831a54aa-cf2b-4d87-af86-e368a5936f6b"
/>

Now:

<img width="354" height="115" alt="image"
src="https://github.com/user-attachments/assets/e5df5e05-0869-4db2-8dee-38611263191c"
/>


Most of the changes in this PR are caused by us requiring `&mut Window`
in `set_placeholder_text`.

Release Notes:

- Fixed an issue where placeholders inside editors would not wrap

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-09-10 15:38:19 +00:00
Joseph T. Lyons
fb3c991112 Bump Zed to v0.205 (#37917)
Release Notes:

-N/A
2025-09-10 14:39:37 +00:00
623 changed files with 39409 additions and 32239 deletions

View File

@@ -26,7 +26,7 @@ third-party = [
# build of remote_server should not include scap / its x11 dependency
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
# build of remote_server should not need to include on libalsa through rodio
{ name = "rodio" },
{ name = "rodio", git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"},
]
[final-excludes]

View File

@@ -1,8 +1,8 @@
name: Bug Report (Windows Alpha)
description: Zed Windows Alpha Related Bugs
name: Bug Report (Windows Beta)
description: Zed Windows Beta Related Bugs
type: "Bug"
labels: ["windows"]
title: "Windows Alpha: <a short description of the Windows bug>"
title: "Windows Beta: <a short description of the Windows bug>"
body:
- type: textarea
attributes:

View File

@@ -373,6 +373,46 @@ jobs:
if: always()
run: rm -rf ./../.cargo
doctests:
# Nextest currently doesn't support doctests, so run them separately and in parallel.
timeout-minutes: 60
name: (Linux) Run doctests
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- namespace-profile-16x32-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# cache-provider: "buildjet"
- name: Install Linux dependencies
run: ./script/linux
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: Run doctests
run: cargo test --workspace --doc --no-fail-fast
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo
build_remote_server:
timeout-minutes: 60
name: (Linux) Build Remote Server

View File

@@ -1,3 +1,6 @@
# IF YOU UPDATE THE NAME OF ANY GITHUB SECRET, YOU MUST CHERRY PICK THE COMMIT
# TO BOTH STABLE AND PREVIEW CHANNELS
name: Release Actions
on:
@@ -13,9 +16,9 @@ jobs:
id: get-release-url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview/latest"
URL="https://zed.dev/releases/preview"
else
URL="https://zed.dev/releases/stable/latest"
URL="https://zed.dev/releases/stable"
fi
echo "URL=$URL" >> "$GITHUB_OUTPUT"
@@ -32,7 +35,7 @@ jobs:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
send_release_notes_email:

57
.github/workflows/congrats.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: Congratsbot
on:
push:
branches: [main]
jobs:
check-author:
if: ${{ github.repository_owner == 'zed-industries' }}
runs-on: ubuntu-latest
outputs:
should_congratulate: ${{ steps.check.outputs.should_congratulate }}
steps:
- name: Get PR info and check if author is external
id: check
uses: actions/github-script@v7
with:
github-token: ${{ secrets.CONGRATSBOT_GITHUB_TOKEN }}
script: |
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha
});
if (prs.length === 0) {
core.setOutput('should_congratulate', 'false');
return;
}
const mergedPR = prs.find(pr => pr.merged_at !== null) || prs[0];
const prAuthor = mergedPR.user.login;
try {
await github.rest.teams.getMembershipForUserInOrg({
org: 'zed-industries',
team_slug: 'staff',
username: prAuthor
});
core.setOutput('should_congratulate', 'false');
} catch (error) {
if (error.status === 404) {
core.setOutput('should_congratulate', 'true');
} else {
console.error(`Error checking team membership: ${error.message}`);
core.setOutput('should_congratulate', 'false');
}
}
congrats:
needs: check-author
if: needs.check-author.outputs.should_congratulate == 'true'
uses: withastro/automation/.github/workflows/congratsbot.yml@main
with:
EMOJIS: 🎉,🎊,🧑‍🚀,🥳,🙌,🚀,🦀,🔥,🚢
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_CONGRATS }}

View File

@@ -0,0 +1,36 @@
name: Good First Issue Notifier
on:
issues:
types: [labeled]
jobs:
handle-good-first-issue:
if: github.event.label.name == 'good first issue' && github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Prepare Discord message
id: prepare-message
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_URL: ${{ github.event.issue.html_url }}
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
run: |
MESSAGE="[${ISSUE_TITLE} (#${ISSUE_NUMBER})](<${ISSUE_URL}>)"
{
echo "message<<EOF"
echo "$MESSAGE"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Discord Webhook Action
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_GOOD_FIRST_ISSUE }}
content: ${{ steps.prepare-message.outputs.message }}

2
.rules
View File

@@ -59,7 +59,7 @@ Trying to update an entity while it's already being updated must be avoided as t
When `read_with`, `update`, or `update_in` are used with an async context, the closure's return value is wrapped in an `anyhow::Result`.
`WeakEntity<T>` is a weak handle. It has `read_with`, `update`, and `update_in` methods that work the same, but always return an `anyhow::Result` so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to eachother they will never be dropped.
`WeakEntity<T>` is a weak handle. It has `read_with`, `update`, and `update_in` methods that work the same, but always return an `anyhow::Result` so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to each other they will never be dropped.
## Concurrency

View File

@@ -1,71 +1,74 @@
# Contributing to Zed
Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor!
Thank you for helping us make Zed better!
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
All activity in Zed forums is subject to our [Code of
Conduct](https://zed.dev/code-of-conduct). Additionally, contributors must sign
our [Contributor License Agreement](https://zed.dev/cla) before their
contributions can be merged.
## Contribution ideas
If you're looking for ideas about what to work on, check out:
Zed is a large project with a number of priorities. We spend most of
our time working on what we believe the product needs, but we also love working
with the community to improve the product in ways we haven't thought of (or had time to get to yet!)
In particular we love PRs that are:
- Fixes to existing bugs and issues.
- Small enhancements to existing features, particularly to make them work for more people.
- Small extra features, like keybindings or actions you miss from other editors or extensions.
- Work towards shipping larger features on our roadmap.
If you're looking for concrete ideas:
- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed.
- Our [top-ranking issues](https://github.com/zed-industries/zed/issues/5393) based on votes by the community.
- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed.
For adding themes or support for a new language to Zed, check out our [docs on developing extensions](https://zed.dev/docs/extensions/developing-extensions).
## Sending changes
## Proposing changes
The Zed culture values working code and synchronous conversations over long
discussion threads.
The best way to propose a change is to [start a discussion on our GitHub repository](https://github.com/zed-industries/zed/discussions).
The best way to get us to take a look at a proposed change is to send a pull
request. We will get back to you (though this sometimes takes longer than we'd
like, sorry).
First, write a short **problem statement**, which _clearly_ and _briefly_ describes the problem you want to solve independently from any specific solution. It doesn't need to be long or formal, but it's difficult to consider a solution in absence of a clear understanding of the problem.
Although we will take a look, we tend to only merge about half the PRs that are
submitted. If you'd like your PR to have the best chance of being merged:
Next, write a short **solution proposal**. How can the problem (or set of problems) you have stated above be addressed? What are the pros and cons of your approach? Again, keep it brief and informal. This isn't a specification, but rather a starting point for a conversation.
- Include a clear description of what you're solving, and why it's important to you.
- Include tests.
- If it changes the UI, attach screenshots or screen recordings.
By effectively engaging with the Zed team and community early in your process, we're better positioned to give you feedback and understand your pull request once you open it. If the first thing we see from you is a big changeset, we're much less likely to respond to it in a timely manner.
The internal advice for reviewers is as follows:
## Pair programming
- If the fix/feature is obviously great, and the code is great. Hit merge.
- If the fix/feature is obviously great, and the code is nearly great. Send PR comments, or offer to pair to get things perfect.
- If the fix/feature is not obviously great, or the code needs rewriting from scratch. Close the PR with a thank you and some explanation.
We plan to set aside time each week to pair program with contributors on promising pull requests in Zed. This will be an experiment. We tend to prefer pairing over async code review on our team, and we'd like to see how well it works in an open source setting. If we're finding it difficult to get on the same page with async review, we may ask you to pair with us if you're open to it. The closer a contribution is to the goals outlined in our roadmap, the more likely we'll be to spend time pairing on it.
If you need more feedback from us: the best way is to be responsive to
Github comments, or to offer up time to pair with us.
## Mandatory PR contents
If you are making a larger change, or need advice on how to finish the change
you're making, please open the PR early. We would love to help you get
things right, and it's often easier to see how to solve a problem before the
diff gets too big.
Please ensure the PR contains
## Things we will (probably) not merge
- Before & after screenshots, if there are visual adjustments introduced.
Although there are few hard and fast rules, typically we don't merge:
Examples of visual adjustments: tree-sitter query updates, UI changes, etc.
- A disclosure of the AI assistance usage, if any was used.
Any kind of AI assistance must be disclosed in the PR, along with the extent to which AI assistance was used (e.g. docs only vs. code generation).
If the PR responses are being generated by an AI, disclose that as well.
As a small exception, trivial tab-completion doesn't need to be disclosed, as long as it's limited to single keywords or short phrases.
## Tips to improve the chances of your PR getting reviewed and merged
- Discuss your plans ahead of time with the team
- Small, focused, incremental pull requests are much easier to review
- Spend time explaining your changes in the pull request body
- Add test coverage and documentation
- Choose tasks that align with our roadmap
- Pair with us and watch us code to learn the codebase
- Low effort PRs, such as those that just re-arrange syntax, won't be merged without a compelling justification
## File icons
Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner.
We do not accept PRs for file icons that are just an off-the-shelf SVG taken from somewhere else.
### Adding new icons to the Zed icon theme
If you would like to add a new icon to the Zed icon theme, [open a Discussion](https://github.com/zed-industries/zed/discussions/new?category=ux-and-design) and we can work with you on getting an icon designed and added to Zed.
- Anything that can be provided by an extension. For example a new language, or theme. For adding themes or support for a new language to Zed, check out our [docs on developing extensions](https://zed.dev/docs/extensions/developing-extensions).
- New file icons. Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner, please don't submit PRs with off-the-shelf SVGs.
- Giant refactorings.
- Non-trivial changes with no tests.
- Features where (in our subjective opinion) the extra complexity isn't worth it for the number of people who will benefit.
- Anything that seems completely AI generated.
## Bird's-eye view of Zed
We suggest you keep the [zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
We suggest you keep the [Zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:

1361
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -52,10 +52,13 @@ members = [
"crates/debugger_tools",
"crates/debugger_ui",
"crates/deepseek",
"crates/denoise",
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/edit_prediction",
"crates/edit_prediction_button",
"crates/edit_prediction_context",
"crates/zeta2_tools",
"crates/editor",
"crates/eval",
"crates/explorer_command_injector",
@@ -148,7 +151,6 @@ members = [
"crates/session",
"crates/settings",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/settings_ui_macros",
"crates/snippet",
"crates/snippet_provider",
@@ -197,6 +199,7 @@ members = [
"crates/zed_actions",
"crates/zed_env_vars",
"crates/zeta",
"crates/zeta2",
"crates/zeta_cli",
"crates/zlog",
"crates/zlog_settings",
@@ -277,6 +280,7 @@ context_server = { path = "crates/context_server" }
copilot = { path = "crates/copilot" }
crashes = { path = "crates/crashes" }
credentials_provider = { path = "crates/credentials_provider" }
crossbeam = "0.8.4"
dap = { path = "crates/dap" }
dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
@@ -311,6 +315,8 @@ icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" }
edit_prediction = { path = "crates/edit_prediction" }
edit_prediction_button = { path = "crates/edit_prediction_button" }
edit_prediction_context = { path = "crates/edit_prediction_context" }
zeta2_tools = { path = "crates/zeta2_tools" }
inspector_ui = { path = "crates/inspector_ui" }
install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
@@ -369,7 +375,7 @@ remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
rodio = { version = "0.21.1", default-features = false }
rodio = { git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"}
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
@@ -426,6 +432,7 @@ zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zed_env_vars = { path = "crates/zed_env_vars" }
zeta = { path = "crates/zeta" }
zeta2 = { path = "crates/zeta2" }
zlog = { path = "crates/zlog" }
zlog_settings = { path = "crates/zlog_settings" }
@@ -433,7 +440,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { version = "0.2.0-alpha.8", features = ["unstable"] }
agent-client-protocol = { version = "0.4.0", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -471,6 +478,7 @@ blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.21"
cfg-if = "1.0.3"
chrono = { version = "0.4", features = ["serde"] }
ciborium = "0.2"
circular-buffer = "1.0"
@@ -580,6 +588,7 @@ pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", re
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
portable-pty = "0.9.0"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
@@ -615,9 +624,8 @@ rustls-platform-verifier = "0.5.0"
scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false }
schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
serde = { version = "1.0.221", features = ["derive", "rc"] }
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.2", features = [
"preserve_order",
"raw_value",
@@ -625,10 +633,12 @@ serde_json_lenient = { version = "0.2", features = [
serde_path_to_error = "0.1.17"
serde_repr = "0.1"
serde_urlencoded = "0.7"
serde_with = "3.4.0"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
simplelog = "0.12.2"
slotmap = "1.0.6"
smallvec = { version = "1.6", features = ["union"] }
smol = "2.0"
sqlformat = "0.2"

11
assets/icons/linux.svg Normal file
View File

@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3010_383)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.71141 7.06133C3.76141 6.47267 3.78341 5.88133 3.81608 5.29133C4.10416 0.190201 11.896 0.190202 12.1841 5.29133C12.2174 5.898 12.2441 6.50333 12.3067 7.10733C12.6951 7.94202 14.3637 11.6214 13.4134 12.006C13.1894 12.096 12.8041 11.7227 12.3694 11.052C12.207 11.9614 11.7273 12.8132 11.0587 13.4467C11.7441 13.68 12.3334 13.998 12.3334 14.3333C12.3334 14.9176 3.66675 14.9257 3.66675 14.3333C3.66675 13.998 4.25608 13.68 4.94141 13.4467C4.26191 12.803 3.82279 11.9657 3.62408 11.056C3.19075 11.724 2.80608 12.096 2.58341 12.006C1.626 11.6185 3.31478 7.90684 3.71141 7.06133Z" stroke="#7B7B7B" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.11822 6.6L7.68822 7.89C7.85822 8.03 8.12822 8.03 8.29822 7.89L9.86822 6.6C10.1382 6.38 9.94822 6 9.56822 6H6.42822C6.04822 6 5.85822 6.38 6.12822 6.6H6.11822Z" fill="#7B7B7B"/>
</g>
<defs>
<clipPath id="clip0_3010_383">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -462,8 +462,8 @@
"ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes",
"back": "pane::GoBack",
"ctrl-alt--": "pane::GoBack",
"ctrl-alt-_": "pane::GoForward",
"forward": "pane::GoForward",
"ctrl-alt-_": "pane::GoForward",
"ctrl-alt-g": "search::SelectNextMatch",
"f3": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPreviousMatch",
@@ -649,7 +649,9 @@
"ctrl-k shift-up": "workspace::SwapPaneUp",
"ctrl-k shift-down": "workspace::SwapPaneDown",
"ctrl-shift-x": "zed::Extensions",
"ctrl-shift-r": "task::Rerun",
// All task parameters are captured and unchanged between reruns by default.
// Use the `"reevaluate_context"` parameter to control this.
"ctrl-shift-r": ["task::Rerun", { "reevaluate_context": false }],
"ctrl-alt-r": "task::Rerun",
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn",
@@ -1073,6 +1075,12 @@
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
}
},
{
"context": "StashList || (StashList > Picker > Editor)",
"bindings": {
"ctrl-shift-backspace": "stash_picker::DropStashItem"
}
},
{
"context": "Terminal",
"bindings": {
@@ -1132,6 +1140,13 @@
"ctrl-enter": "menu::Confirm"
}
},
{
"context": "ContextServerToolsModal",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel"
}
},
{
"context": "OnboardingAiConfigurationModal",
"use_key_equivalents": true,

View File

@@ -724,7 +724,9 @@
"bindings": {
"cmd-n": "workspace::NewFile",
"cmd-shift-r": "task::Spawn",
"cmd-alt-r": "task::Rerun",
// All task parameters are captured and unchanged between reruns by default.
// Use the `"reevaluate_context"` parameter to control this.
"cmd-alt-r": ["task::Rerun", { "reevaluate_context": false }],
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
@@ -1144,6 +1146,13 @@
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
}
},
{
"context": "StashList || (StashList > Picker > Editor)",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-backspace": "stash_picker::DropStashItem"
}
},
{
"context": "Terminal",
"use_key_equivalents": true,
@@ -1235,6 +1244,13 @@
"cmd-enter": "menu::Confirm"
}
},
{
"context": "ContextServerToolsModal",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel"
}
},
{
"context": "OnboardingAiConfigurationModal",
"use_key_equivalents": true,

View File

@@ -497,6 +497,8 @@
"shift-alt-down": "editor::DuplicateLineDown",
"shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand selection
"shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand selection (VSCode version)
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink selection (VSCode version)
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
@@ -642,7 +644,9 @@
"ctrl-k shift-up": "workspace::SwapPaneUp",
"ctrl-k shift-down": "workspace::SwapPaneDown",
"ctrl-shift-x": "zed::Extensions",
"ctrl-shift-r": "task::Rerun",
// All task parameters are captured and unchanged between reruns by default.
// Use the `"reevaluate_context"` parameter to control this.
"ctrl-shift-r": ["task::Rerun", { "reevaluate_context": false }],
"alt-t": "task::Rerun",
"shift-alt-t": "task::Spawn",
"shift-alt-r": ["task::Spawn", { "reveal_target": "center" }],
@@ -1090,6 +1094,13 @@
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
}
},
{
"context": "StashList || (StashList > Picker > Editor)",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-backspace": "stash_picker::DropStashItem"
}
},
{
"context": "Terminal",
"use_key_equivalents": true,
@@ -1149,6 +1160,13 @@
"ctrl-enter": "menu::Confirm"
}
},
{
"context": "ContextServerToolsModal",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel"
}
},
{
"context": "OnboardingAiConfigurationModal",
"use_key_equivalents": true,

View File

@@ -325,6 +325,27 @@
"\"": "vim::PushRegister"
}
},
{
"context": "vim_mode == helix_select",
"bindings": {
"v": "vim::NormalBefore",
";": "vim::HelixCollapseSelection",
"~": "vim::ChangeCase",
"ctrl-a": "vim::Increment",
"ctrl-x": "vim::Decrement",
"shift-j": "vim::JoinLines",
"i": "vim::InsertBefore",
"a": "vim::InsertAfter",
"p": "vim::Paste",
"u": "vim::Undo",
"r": "vim::PushReplace",
"s": "vim::Substitute",
"ctrl-pageup": "pane::ActivatePreviousItem",
"ctrl-pagedown": "pane::ActivateNextItem",
".": "vim::Repeat",
"alt-.": "vim::RepeatFind"
}
},
{
"context": "vim_mode == insert",
"bindings": {
@@ -396,7 +417,12 @@
"bindings": {
"i": "vim::HelixInsert",
"a": "vim::HelixAppend",
"ctrl-[": "editor::Cancel",
"ctrl-[": "editor::Cancel"
}
},
{
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
"bindings": {
";": "vim::HelixCollapseSelection",
":": "command_palette::Toggle",
"m": "vim::PushHelixMatch",
@@ -416,9 +442,8 @@
">": "vim::Indent",
"<": "vim::Outdent",
"=": "vim::AutoIndent",
"g u": "vim::PushLowercase",
"g shift-u": "vim::PushUppercase",
"g ~": "vim::PushOppositeCase",
"`": "vim::ConvertToLowerCase",
"alt-`": "vim::ConvertToUpperCase",
"g q": "vim::PushRewrap",
"g w": "vim::PushRewrap",
"insert": "vim::InsertBefore",

View File

@@ -311,7 +311,7 @@
// bracket, brace, single or double quote characters.
// For example, when you select text and type (, Zed will surround the text with ().
"use_auto_surround": true,
/// Whether indentation should be adjusted based on the context whilst typing.
// Whether indentation should be adjusted based on the context whilst typing.
"auto_indent": true,
// Whether indentation of pasted content should be adjusted based on the context.
"auto_indent_on_paste": true,
@@ -362,6 +362,11 @@
// - It is adjacent to an edge (start or end)
// - It is adjacent to a whitespace (left or right)
"show_whitespaces": "selection",
// Visible characters used to render whitespace when show_whitespaces is enabled.
"whitespace_map": {
"space": "•",
"tab": "→"
},
// Settings related to calls in Zed
"calls": {
// Join calls with the microphone live by default
@@ -386,6 +391,8 @@
"use_system_window_tabs": false,
// Titlebar related settings
"title_bar": {
// When to show the title bar: "always" | "never" | "hide_in_full_screen".
"show": "always",
// Whether to show the branch icon beside branch switcher in the titlebar.
"show_branch_icon": false,
// Whether to show the branch name button in the titlebar.
@@ -401,6 +408,21 @@
// Whether to show the menus in the titlebar.
"show_menus": false
},
"audio": {
// Opt into the new audio system.
"experimental.rodio_audio": false,
// Requires 'rodio_audio: true'
//
// Use the new audio systems automatic gain control for your microphone.
// This affects how loud you sound to others.
"experimental.control_input_volume": false,
// Requires 'rodio_audio: true'
//
// Use the new audio systems automatic gain control on everyone in the
// call. This makes call members who are too quite louder and those who are
// too loud quieter. This only affects how things sound for you.
"experimental.control_output_volume": false
},
// Scrollbar related settings
"scrollbar": {
// When to show the scrollbar in the editor.
@@ -581,6 +603,7 @@
// Toggle certain types of hints on and off, all switched on by default.
"show_type_hints": true,
"show_parameter_hints": true,
"show_value_hints": true,
// Corresponds to null/None LSP hint type value.
"show_other_hints": true,
// Whether to show a background for inlay hints.
@@ -789,7 +812,7 @@
"agent": {
// Whether the agent is enabled.
"enabled": true,
/// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
"preferred_completion_mode": "normal",
// Whether to show the agent panel button in the status bar.
"button": true,
@@ -799,6 +822,8 @@
"default_width": 640,
// Default height when the agent panel is docked to the bottom.
"default_height": 320,
// The view to use by default (thread, or text_thread)
"default_view": "thread",
// The default model to use when creating new threads.
"default_model": {
// The provider to use.
@@ -900,22 +925,22 @@
// Default: false
"play_sound_when_agent_done": false,
/// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
///
/// Default: true
// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
//
// Default: true
"expand_edit_card": true,
/// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
///
/// Default: true
"expand_terminal_card": true
},
// The settings for slash commands.
"slash_commands": {
// Settings for the `/project` slash command.
"project": {
// Whether `/project` is enabled.
"enabled": false
}
// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
//
// Default: true
"expand_terminal_card": true,
// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
//
// Default: false
"use_modifier_to_send": false,
// Minimum number of lines to display in the agent message editor.
//
// Default: 4
"message_editor_min_lines": 4
},
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
@@ -928,6 +953,7 @@
//
// This is typically customized on a per-language basis.
"language_servers": ["..."],
// When to automatically save edited buffers. This setting can
// take four values.
//
@@ -1259,7 +1285,13 @@
// },
// Whether edit predictions are enabled when editing text threads.
// This setting has no effect if globally disabled.
"enabled_in_text_threads": true
"enabled_in_text_threads": true,
"copilot": {
"enterprise_uri": null,
"proxy": null,
"proxy_no_verify": null
}
},
// Settings specific to journaling
"journal": {
@@ -1687,6 +1719,11 @@
"allow_rewrap": "anywhere"
},
"Python": {
"formatter": {
"language_server": {
"name": "ruff"
}
},
"debuggers": ["Debugpy"]
},
"Ruby": {
@@ -1757,6 +1794,7 @@
"anthropic": {
"api_url": "https://api.anthropic.com"
},
"bedrock": {},
"google": {
"api_url": "https://generativelanguage.googleapis.com"
},
@@ -1778,14 +1816,30 @@
},
"mistral": {
"api_url": "https://api.mistral.ai/v1"
}
},
"vercel": {
"api_url": "https://api.v0.dev/v1"
},
"x_ai": {
"api_url": "https://api.x.ai/v1"
},
"zed.dev": {}
},
"session": {
// Whether or not to restore unsaved buffers on restart.
//
// If this is true, user won't be prompted whether to save/discard
// dirty files when closing the application.
//
// Default: true
"restore_unsaved_buffers": true
},
// Zed's Prettier integration settings.
// Allows to enable/disable formatting with Prettier
// and configure default Prettier, used when no project-level Prettier installation is found.
"prettier": {
// // Whether to consider prettier formatter or not when attempting to format a file.
// "allowed": false,
"allowed": false
//
// // Use regular Prettier json configuration.
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
@@ -1818,6 +1872,10 @@
// }
// }
},
// DAP Specific settings.
"dap": {
// Specify the DAP name as a key here.
},
// Common language server settings.
"global_lsp_settings": {
// Whether to show the LSP servers button in the status bar.
@@ -1825,13 +1883,23 @@
},
// Jupyter settings
"jupyter": {
"enabled": true
"enabled": true,
"kernel_selections": {}
// Specify the language name as the key and the kernel name as the value.
// "kernel_selections": {
// "python": "conda-base"
// "typescript": "deno"
// }
},
// REPL settings.
"repl": {
// Maximum number of columns to keep in REPL's scrollback buffer.
// Clamped with [20, 512] range.
"max_columns": 128,
// Maximum number of lines to keep in REPL's scrollback buffer.
// Clamped with [4, 256] range.
"max_lines": 32
},
// Vim settings
"vim": {
"default_mode": "normal",
@@ -1947,5 +2015,11 @@
// }
// }
// }
"profiles": []
"profiles": [],
// A map of log scopes to the desired log level.
// Useful for filtering out noisy logs or enabling more verbose logging.
//
// Example: {"log": {"client": "warn"}}
"log": {}
}

View File

@@ -45,7 +45,6 @@ url.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
which.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

View File

@@ -7,12 +7,12 @@ use agent_settings::AgentSettings;
use collections::HashSet;
pub use connection::*;
pub use diff::*;
use futures::future::Shared;
use language::language_settings::FormatOnSave;
pub use mention::*;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use task::{Shell, ShellBuilder};
pub use terminal::*;
use action_log::ActionLog;
@@ -34,7 +34,7 @@ use std::rc::Rc;
use std::time::{Duration, Instant};
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
use ui::App;
use util::{ResultExt, get_system_shell};
use util::{ResultExt, get_default_system_shell};
use uuid::Uuid;
#[derive(Debug)]
@@ -786,7 +786,6 @@ pub struct AcpThread {
token_usage: Option<TokenUsage>,
prompt_capabilities: acp::PromptCapabilities,
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
determine_shell: Shared<Task<String>>,
terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
}
@@ -862,7 +861,7 @@ impl AcpThread {
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
cx: &mut Context<Self>,
) -> Self {
let prompt_capabilities = *prompt_capabilities_rx.borrow();
let prompt_capabilities = prompt_capabilities_rx.borrow().clone();
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
loop {
let caps = prompt_capabilities_rx.recv().await?;
@@ -873,20 +872,6 @@ impl AcpThread {
}
});
let determine_shell = cx
.background_spawn(async move {
if cfg!(windows) {
return get_system_shell();
}
if which::which("bash").is_ok() {
"bash".into()
} else {
get_system_shell()
}
})
.shared();
Self {
action_log,
shared_buffers: Default::default(),
@@ -901,12 +886,11 @@ impl AcpThread {
prompt_capabilities,
_observe_prompt_capabilities: task,
terminals: HashMap::default(),
determine_shell,
}
}
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
self.prompt_capabilities
self.prompt_capabilities.clone()
}
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
@@ -1127,9 +1111,33 @@ impl AcpThread {
let update = update.into();
let languages = self.project.read(cx).languages().clone();
let ix = self
.index_for_tool_call(update.id())
.context("Tool call not found")?;
let ix = match self.index_for_tool_call(update.id()) {
Some(ix) => ix,
None => {
// Tool call not found - create a failed tool call entry
let failed_tool_call = ToolCall {
id: update.id().clone(),
label: cx.new(|cx| Markdown::new("Tool call not found".into(), None, None, cx)),
kind: acp::ToolKind::Fetch,
content: vec![ToolCallContent::ContentBlock(ContentBlock::new(
acp::ContentBlock::Text(acp::TextContent {
text: "Tool call not found".to_string(),
annotations: None,
meta: None,
}),
&languages,
cx,
))],
status: ToolCallStatus::Failed,
locations: Vec::new(),
resolved_locations: Vec::new(),
raw_input: None,
raw_output: None,
};
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
return Ok(());
}
};
let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
unreachable!()
};
@@ -1446,6 +1454,7 @@ impl AcpThread {
vec![acp::ContentBlock::Text(acp::TextContent {
text: message.to_string(),
annotations: None,
meta: None,
})],
cx,
)
@@ -1464,6 +1473,7 @@ impl AcpThread {
let request = acp::PromptRequest {
prompt: message.clone(),
session_id: self.session_id.clone(),
meta: None,
};
let git_store = self.project.read(cx).git_store().clone();
@@ -1555,7 +1565,8 @@ impl AcpThread {
let canceled = matches!(
result,
Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled
stop_reason: acp::StopReason::Cancelled,
meta: None,
}))
);
@@ -1571,6 +1582,7 @@ impl AcpThread {
// Handle refusal - distinguish between user prompt and tool call refusals
if let Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: _,
})) = result
{
if let Some((user_msg_ix, _)) = this.last_user_message() {
@@ -1769,6 +1781,9 @@ impl AcpThread {
reuse_shared_snapshot: bool,
cx: &mut Context<Self>,
) -> Task<Result<String>> {
// Args are 1-based, move to 0-based
let line = line.unwrap_or_default().saturating_sub(1);
let limit = limit.unwrap_or(u32::MAX);
let project = self.project.clone();
let action_log = self.action_log.clone();
cx.spawn(async move |this, cx| {
@@ -1796,44 +1811,37 @@ impl AcpThread {
action_log.update(cx, |action_log, cx| {
action_log.buffer_read(buffer.clone(), cx);
})?;
project.update(cx, |project, cx| {
let position = buffer
.read(cx)
.snapshot()
.anchor_before(Point::new(line.unwrap_or_default(), 0));
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position,
}),
cx,
);
})?;
buffer.update(cx, |buffer, _| buffer.snapshot())?
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
this.update(cx, |this, _| {
this.shared_buffers.insert(buffer.clone(), snapshot.clone());
})?;
snapshot
};
this.update(cx, |this, _| {
let text = snapshot.text();
this.shared_buffers.insert(buffer.clone(), snapshot);
if line.is_none() && limit.is_none() {
return Ok(text);
}
let limit = limit.unwrap_or(u32::MAX) as usize;
let Some(line) = line else {
return Ok(text.lines().take(limit).collect::<String>());
};
let max_point = snapshot.max_point();
if line >= max_point.row {
anyhow::bail!(
"Attempting to read beyond the end of the file, line {}:{}",
max_point.row + 1,
max_point.column
);
}
let count = text.lines().count();
if count < line as usize {
anyhow::bail!("There are only {} lines", count);
}
Ok(text
.lines()
.skip(line as usize + 1)
.take(limit)
.collect::<String>())
})?
let start = snapshot.anchor_before(Point::new(line, 0));
let end = snapshot.anchor_before(Point::new(line.saturating_add(limit), 0));
project.update(cx, |project, cx| {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: start,
}),
cx,
);
})?;
Ok(snapshot.text_for_range(start..end).collect::<String>())
})
}
@@ -1936,28 +1944,13 @@ impl AcpThread {
pub fn create_terminal(
&self,
mut command: String,
command: String,
args: Vec<String>,
extra_env: Vec<acp::EnvVariable>,
cwd: Option<PathBuf>,
output_byte_limit: Option<u64>,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Terminal>>> {
for arg in args {
command.push(' ');
command.push_str(&arg);
}
let shell_command = if cfg!(windows) {
format!("$null | & {{{}}}", command.replace("\"", "'"))
} else if let Some(cwd) = cwd.as_ref().and_then(|cwd| cwd.as_os_str().to_str()) {
// Make sure once we're *inside* the shell, we cd into `cwd`
format!("(cd {cwd}; {}) </dev/null", command)
} else {
format!("({}) </dev/null", command)
};
let args = vec!["-c".into(), shell_command];
let env = match &cwd {
Some(dir) => self.project.update(cx, |project, cx| {
project.directory_environment(dir.as_path().into(), cx)
@@ -1978,20 +1971,30 @@ impl AcpThread {
let project = self.project.clone();
let language_registry = project.read(cx).languages().clone();
let determine_shell = self.determine_shell.clone();
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
let terminal_task = cx.spawn({
let terminal_id = terminal_id.clone();
async move |_this, cx| {
let program = determine_shell.await;
let env = env.await;
let (command, args) = ShellBuilder::new(
project
.update(cx, |project, cx| {
project
.remote_client()
.and_then(|r| r.read(cx).default_system_shell())
})?
.as_deref(),
&Shell::Program(get_default_system_shell()),
)
.redirect_stdin_to_dev_null()
.build(Some(command), &args);
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_task(
task::SpawnInTerminal {
command: Some(program),
args,
command: Some(command.clone()),
args: args.clone(),
cwd: cwd.clone(),
env,
..Default::default()
@@ -2004,7 +2007,7 @@ impl AcpThread {
cx.new(|cx| {
Terminal::new(
terminal_id,
command,
&format!("{} {}", command, args.join(" ")),
cwd,
output_byte_limit.map(|l| l as usize),
terminal,
@@ -2163,6 +2166,7 @@ mod tests {
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "Hello, ".to_string(),
meta: None,
}),
cx,
);
@@ -2186,6 +2190,7 @@ mod tests {
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "world!".to_string(),
meta: None,
}),
cx,
);
@@ -2207,6 +2212,7 @@ mod tests {
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "Assistant response".to_string(),
meta: None,
}),
false,
cx,
@@ -2220,6 +2226,7 @@ mod tests {
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "New user message".to_string(),
meta: None,
}),
cx,
);
@@ -2265,6 +2272,7 @@ mod tests {
})?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@@ -2335,6 +2343,7 @@ mod tests {
.unwrap();
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@@ -2378,6 +2387,82 @@ mod tests {
request.await.unwrap();
}
#[gpui::test]
async fn test_reading_from_line(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\nfour\n"}))
.await;
let project = Project::test(fs.clone(), [], cx).await;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
})
.await
.unwrap();
let connection = Rc::new(FakeAgentConnection::new());
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx))
.await
.unwrap();
// Whole file
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx)
})
.await
.unwrap();
assert_eq!(content, "one\ntwo\nthree\nfour\n");
// Only start line
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(path!("/tmp/foo").into(), Some(3), None, false, cx)
})
.await
.unwrap();
assert_eq!(content, "three\nfour\n");
// Only limit
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx)
})
.await
.unwrap();
assert_eq!(content, "one\ntwo\n");
// Range
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(path!("/tmp/foo").into(), Some(2), Some(2), false, cx)
})
.await
.unwrap();
assert_eq!(content, "two\nthree\n");
// Invalid
let err = thread
.update(cx, |thread, cx| {
thread.read_text_file(path!("/tmp/foo").into(), Some(5), Some(2), false, cx)
})
.await
.unwrap_err();
assert_eq!(
err.to_string(),
"Attempting to read beyond the end of the file, line 5:0"
);
}
#[gpui::test]
async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) {
init_test(cx);
@@ -2403,6 +2488,7 @@ mod tests {
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
}),
cx,
)
@@ -2411,6 +2497,7 @@ mod tests {
.unwrap();
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@@ -2459,6 +2546,7 @@ mod tests {
status: Some(acp::ToolCallStatus::Completed),
..Default::default()
},
meta: None,
}),
cx,
)
@@ -2501,11 +2589,13 @@ mod tests {
path: "/test/test.txt".into(),
old_text: None,
new_text: "foo".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
}),
cx,
)
@@ -2514,6 +2604,7 @@ mod tests {
.unwrap();
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@@ -2576,6 +2667,7 @@ mod tests {
})?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@@ -2743,6 +2835,7 @@ mod tests {
raw_output: Some(
serde_json::json!({"result": "inappropriate content"}),
),
meta: None,
}),
cx,
)
@@ -2752,10 +2845,12 @@ mod tests {
// Now return refusal because of the tool result
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
})
} else {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
}
@@ -2789,6 +2884,7 @@ mod tests {
vec![acp::ContentBlock::Text(acp::TextContent {
text: "Hello".into(),
annotations: None,
meta: None,
})],
cx,
)
@@ -2841,6 +2937,7 @@ mod tests {
async move {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
})
}
.boxed_local()
@@ -2848,6 +2945,7 @@ mod tests {
async move {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@@ -2909,6 +3007,7 @@ mod tests {
if refuse_next.load(SeqCst) {
return Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
});
}
@@ -2927,6 +3026,7 @@ mod tests {
})?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
@@ -3082,6 +3182,7 @@ mod tests {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
@@ -3113,6 +3214,7 @@ mod tests {
} else {
Task::ready(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
}))
}
}
@@ -3154,4 +3256,65 @@ mod tests {
Task::ready(Ok(()))
}
}
#[gpui::test]
async fn test_tool_call_not_found_creates_failed_entry(cx: &mut 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_thread(project, Path::new(path!("/test")), cx))
.await
.unwrap();
// Try to update a tool call that doesn't exist
let nonexistent_id = acp::ToolCallId("nonexistent-tool-call".into());
thread.update(cx, |thread, cx| {
let result = thread.handle_session_update(
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
id: nonexistent_id.clone(),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
..Default::default()
},
meta: None,
}),
cx,
);
// The update should succeed (not return an error)
assert!(result.is_ok());
// There should now be exactly one entry in the thread
assert_eq!(thread.entries.len(), 1);
// The entry should be a failed tool call
if let AgentThreadEntry::ToolCall(tool_call) = &thread.entries[0] {
assert_eq!(tool_call.id, nonexistent_id);
assert!(matches!(tool_call.status, ToolCallStatus::Failed));
assert_eq!(tool_call.kind, acp::ToolKind::Fetch);
// Check that the content contains the error message
assert_eq!(tool_call.content.len(), 1);
if let ToolCallContent::ContentBlock(content_block) = &tool_call.content[0] {
match content_block {
ContentBlock::Markdown { markdown } => {
let markdown_text = markdown.read(cx).source();
assert!(markdown_text.contains("Tool call not found"));
}
ContentBlock::Empty => panic!("Expected markdown content, got empty"),
ContentBlock::ResourceLink { .. } => {
panic!("Expected markdown content, got resource link")
}
}
} else {
panic!("Expected ContentBlock, got: {:?}", tool_call.content[0]);
}
} else {
panic!("Expected ToolCall entry, got: {:?}", thread.entries[0]);
}
});
}
}

View File

@@ -354,6 +354,7 @@ mod test_support {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
@@ -393,7 +394,10 @@ mod test_support {
response_tx.replace(tx);
cx.spawn(async move |_| {
let stop_reason = rx.await?;
Ok(acp::PromptResponse { stop_reason })
Ok(acp::PromptResponse {
stop_reason,
meta: None,
})
})
} else {
for update in self.next_prompt_updates.lock().drain(..) {
@@ -432,6 +436,7 @@ mod test_support {
try_join_all(tasks).await?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
})
}

View File

@@ -162,7 +162,7 @@ impl MentionUri {
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
}
MentionUri::PastedImage => IconName::Image.path().into(),
MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
MentionUri::Directory { abs_path } => FileIcons::get_folder_icon(false, abs_path, cx)
.unwrap_or_else(|| IconName::Folder.path().into()),
MentionUri::Symbol { .. } => IconName::Code.path().into(),
MentionUri::Thread { .. } => IconName::Thread.path().into(),

View File

@@ -28,7 +28,7 @@ pub struct TerminalOutput {
impl Terminal {
pub fn new(
id: acp::TerminalId,
command: String,
command_label: &str,
working_dir: Option<PathBuf>,
output_byte_limit: Option<usize>,
terminal: Entity<terminal::Terminal>,
@@ -40,7 +40,7 @@ impl Terminal {
id,
command: cx.new(|cx| {
Markdown::new(
format!("```\n{}\n```", command).into(),
format!("```\n{}\n```", command_label).into(),
Some(language_registry.clone()),
None,
cx,
@@ -75,6 +75,7 @@ impl Terminal {
acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
meta: None,
}
})
.shared(),
@@ -105,7 +106,9 @@ impl Terminal {
exit_status: Some(acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
meta: None,
}),
meta: None,
}
} else {
let (current_content, original_len) = self.truncated_output(cx);
@@ -114,6 +117,7 @@ impl Terminal {
truncated: current_content.len() < original_len,
output: current_content,
exit_status: None,
meta: None,
}
}
}

View File

@@ -1,4 +1,4 @@
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissMessage, VersionCheckType};
use editor::Editor;
use extension_host::{ExtensionOperation, ExtensionStore};
use futures::StreamExt;
@@ -280,18 +280,13 @@ impl ActivityIndicator {
});
}
fn dismiss_error_message(
&mut self,
_: &DismissErrorMessage,
_: &mut Window,
cx: &mut Context<Self>,
) {
let error_dismissed = if let Some(updater) = &self.auto_updater {
updater.update(cx, |updater, cx| updater.dismiss_error(cx))
fn dismiss_message(&mut self, _: &DismissMessage, _: &mut Window, cx: &mut Context<Self>) {
let dismissed = if let Some(updater) = &self.auto_updater {
updater.update(cx, |updater, cx| updater.dismiss(cx))
} else {
false
};
if error_dismissed {
if dismissed {
return;
}
@@ -513,7 +508,7 @@ impl ActivityIndicator {
on_click: Some(Arc::new(move |this, window, cx| {
this.statuses
.retain(|status| !downloading.contains(&status.name));
this.dismiss_error_message(&DismissErrorMessage, window, cx)
this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: None,
});
@@ -542,7 +537,7 @@ impl ActivityIndicator {
on_click: Some(Arc::new(move |this, window, cx| {
this.statuses
.retain(|status| !checking_for_update.contains(&status.name));
this.dismiss_error_message(&DismissErrorMessage, window, cx)
this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: None,
});
@@ -650,13 +645,14 @@ impl ActivityIndicator {
.and_then(|updater| match &updater.read(cx).status() {
AutoUpdateStatus::Checking => Some(Content {
icon: Some(
Icon::new(IconName::Download)
Icon::new(IconName::LoadCircle)
.size(IconSize::Small)
.with_rotate_animation(3)
.into_any_element(),
),
message: "Checking for Zed updates…".to_string(),
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: None,
}),
@@ -668,19 +664,20 @@ impl ActivityIndicator {
),
message: "Downloading Zed update…".to_string(),
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: Some(Self::version_tooltip_message(version)),
}),
AutoUpdateStatus::Installing { version } => Some(Content {
icon: Some(
Icon::new(IconName::Download)
Icon::new(IconName::LoadCircle)
.size(IconSize::Small)
.with_rotate_animation(3)
.into_any_element(),
),
message: "Installing Zed update…".to_string(),
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: Some(Self::version_tooltip_message(version)),
}),
@@ -690,17 +687,18 @@ impl ActivityIndicator {
on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
tooltip_message: Some(Self::version_tooltip_message(version)),
}),
AutoUpdateStatus::Errored => Some(Content {
AutoUpdateStatus::Errored { error } => Some(Content {
icon: Some(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.into_any_element(),
),
message: "Auto update failed".to_string(),
message: "Failed to update Zed".to_string(),
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
window.dispatch_action(Box::new(workspace::OpenLog), cx);
this.dismiss_message(&DismissMessage, window, cx);
})),
tooltip_message: None,
tooltip_message: Some(format!("{error}")),
}),
AutoUpdateStatus::Idle => None,
})
@@ -738,7 +736,7 @@ impl ActivityIndicator {
})),
message,
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&Default::default(), window, cx)
this.dismiss_message(&Default::default(), window, cx)
})),
tooltip_message: None,
})
@@ -777,7 +775,7 @@ impl Render for ActivityIndicator {
let result = h_flex()
.id("activity-indicator")
.on_action(cx.listener(Self::show_error_message))
.on_action(cx.listener(Self::dismiss_error_message));
.on_action(cx.listener(Self::dismiss_message));
let Some(content) = self.content_to_render(cx) else {
return result;
};

View File

@@ -49,10 +49,10 @@ impl AgentProfile {
.unwrap_or_default(),
};
update_settings_file::<AgentSettings>(fs, cx, {
update_settings_file(fs, cx, {
let id = id.clone();
move |settings, _cx| {
settings.create_profile(id, profile_settings).log_err();
profile_settings.save_to_settings(id, settings).log_err();
}
});

View File

@@ -6,7 +6,7 @@ use futures::future;
use futures::{FutureExt, future::Shared};
use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task};
use icons::IconName;
use language::{Buffer, ParseStatus};
use language::Buffer;
use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
use project::{Project, ProjectEntryId, ProjectPath, Worktree};
use prompt_store::{PromptStore, UserPromptId};
@@ -191,46 +191,19 @@ impl FileContextHandle {
let buffer = self.buffer.clone();
cx.spawn(async move |cx| {
// For large files, use outline instead of full content
if rope.len() > outline::AUTO_OUTLINE_SIZE {
// Wait until the buffer has been fully parsed, so we can read its outline
if let Ok(mut parse_status) =
buffer.read_with(cx, |buffer, _| buffer.parse_status())
{
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await.log_err();
}
let buffer_content =
outline::get_buffer_content_or_outline(buffer.clone(), Some(&full_path), &cx)
.await
.unwrap_or_else(|_| outline::BufferContent {
text: rope.to_string(),
is_outline: false,
});
if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot())
&& let Some(outline) = snapshot.outline(None)
{
let items = outline
.items
.into_iter()
.map(|item| item.to_point(&snapshot));
if let Ok(outline_text) =
outline::render_outline(items, None, 0, usize::MAX).await
{
let context = AgentContext::File(FileContext {
handle: self,
full_path,
text: outline_text.into(),
is_outline: true,
});
return Some((context, vec![buffer]));
}
}
}
}
// Fallback to full content if we couldn't build an outline
// (or didn't need to because the file was small enough)
let context = AgentContext::File(FileContext {
handle: self,
full_path,
text: rope.to_string().into(),
is_outline: false,
text: buffer_content.text.into(),
is_outline: buffer_content.is_outline,
});
Some((context, vec![buffer]))
})

View File

@@ -1,7 +1,4 @@
use crate::{
ThreadId,
thread_store::{SerializedThreadMetadata, ThreadStore},
};
use crate::{ThreadId, thread_store::SerializedThreadMetadata};
use anyhow::{Context as _, Result};
use assistant_context::SavedContextMetadata;
use chrono::{DateTime, Utc};
@@ -61,7 +58,6 @@ enum SerializedRecentOpen {
}
pub struct HistoryStore {
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>,
_subscriptions: Vec<gpui::Subscription>,
@@ -70,15 +66,11 @@ pub struct HistoryStore {
impl HistoryStore {
pub fn new(
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context::ContextStore>,
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![
cx.observe(&thread_store, |_, _, cx| cx.notify()),
cx.observe(&context_store, |_, _, cx| cx.notify()),
];
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())];
cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
@@ -96,7 +88,6 @@ impl HistoryStore {
.detach();
Self {
thread_store,
context_store,
recently_opened_entries: initial_recent_entries.into_iter().collect(),
_subscriptions: subscriptions,
@@ -112,13 +103,6 @@ impl HistoryStore {
return history_entries;
}
history_entries.extend(
self.thread_store
.read(cx)
.reverse_chronological_threads()
.cloned()
.map(HistoryEntry::Thread),
);
history_entries.extend(
self.context_store
.read(cx)
@@ -141,22 +125,6 @@ impl HistoryStore {
return Vec::new();
}
let thread_entries = self
.thread_store
.read(cx)
.reverse_chronological_threads()
.flat_map(|thread| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::Thread(id) if &thread.id == id => {
Some((index, HistoryEntry::Thread(thread.clone())))
}
_ => None,
})
});
let context_entries =
self.context_store
.read(cx)
@@ -173,8 +141,7 @@ impl HistoryStore {
})
});
thread_entries
.chain(context_entries)
context_entries
// optimization to halt iteration early
.take(self.recently_opened_entries.len())
.sorted_unstable_by_key(|(index, _)| *index)

View File

@@ -3272,7 +3272,7 @@ mod tests {
// Test-specific constants
const TEST_RATE_LIMIT_RETRY_SECS: u64 = 30;
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters};
use agent_settings::{AgentProfileId, AgentSettings};
use assistant_tool::ToolRegistry;
use assistant_tools;
use futures::StreamExt;
@@ -3289,7 +3289,7 @@ mod tests {
use project::{FakeFs, Project};
use prompt_store::PromptBuilder;
use serde_json::json;
use settings::{Settings, SettingsStore};
use settings::{LanguageModelParameters, Settings, SettingsStore};
use std::sync::Arc;
use std::time::Duration;
use theme::ThemeSettings;

View File

@@ -6,7 +6,6 @@ use crate::{HistoryStore, TerminalHandle, ThreadEnvironment, TitleUpdated, Token
use acp_thread::{AcpThread, AgentModelSelector};
use action_log::ActionLog;
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result, anyhow};
use collections::{HashSet, IndexMap};
use fs::Fs;
@@ -21,7 +20,7 @@ use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{
ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext,
};
use settings::update_settings_file;
use settings::{LanguageModelSelection, update_settings_file};
use std::any::Any;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
@@ -166,33 +165,41 @@ impl LanguageModels {
cx.background_spawn(async move {
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
if let Err(err) = authenticate_task.await {
if matches!(err, language_model::AuthenticateError::CredentialsNotFound) {
// Since we're authenticating these providers in the
// background for the purposes of populating the
// language selector, we don't care about providers
// where the credentials are not found.
} else {
// Some providers have noisy failure states that we
// don't want to spam the logs with every time the
// language model selector is initialized.
//
// Ideally these should have more clear failure modes
// that we know are safe to ignore here, like what we do
// with `CredentialsNotFound` above.
match provider_id.0.as_ref() {
"lmstudio" | "ollama" => {
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
//
// These fail noisily, so we don't log them.
}
"copilot_chat" => {
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
}
_ => {
log::error!(
"Failed to authenticate provider: {}: {err}",
provider_name.0
);
match err {
language_model::AuthenticateError::CredentialsNotFound => {
// Since we're authenticating these providers in the
// background for the purposes of populating the
// language selector, we don't care about providers
// where the credentials are not found.
}
language_model::AuthenticateError::ConnectionRefused => {
// Not logging connection refused errors as they are mostly from LM Studio's noisy auth failures.
// LM Studio only has one auth method (endpoint call) which fails for users who haven't enabled it.
// TODO: Better manage LM Studio auth logic to avoid these noisy failures.
}
_ => {
// Some providers have noisy failure states that we
// don't want to spam the logs with every time the
// language model selector is initialized.
//
// Ideally these should have more clear failure modes
// that we know are safe to ignore here, like what we do
// with `CredentialsNotFound` above.
match provider_id.0.as_ref() {
"lmstudio" | "ollama" => {
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
//
// These fail noisily, so we don't log them.
}
"copilot_chat" => {
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
}
_ => {
log::error!(
"Failed to authenticate provider: {}: {err}",
provider_name.0
);
}
}
}
}
@@ -747,6 +754,7 @@ impl NativeAgentConnection {
acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
false,
cx,
@@ -759,6 +767,7 @@ impl NativeAgentConnection {
acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
true,
cx,
@@ -804,7 +813,10 @@ impl NativeAgentConnection {
}
ThreadEvent::Stop(stop_reason) => {
log::debug!("Assistant message complete: {:?}", stop_reason);
return Ok(acp::PromptResponse { stop_reason });
return Ok(acp::PromptResponse {
stop_reason,
meta: None,
});
}
}
}
@@ -818,6 +830,7 @@ impl NativeAgentConnection {
log::debug!("Response stream completed");
anyhow::Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
})
}
@@ -859,13 +872,17 @@ impl AgentModelSelector for NativeAgentConnection {
thread.set_model(model.clone(), cx);
});
update_settings_file::<AgentSettings>(
self.0.read(cx).fs.clone(),
cx,
move |settings, _cx| {
settings.set_model(model);
},
);
update_settings_file(self.0.read(cx).fs.clone(), cx, move |settings, _cx| {
let provider = model.provider_id().0.to_string();
let model = model.id().0.to_string();
settings
.agent
.get_or_insert_default()
.set_model(LanguageModelSelection {
provider: provider.into(),
model,
});
});
Task::ready(Ok(()))
}
@@ -1441,6 +1458,7 @@ mod tests {
mime_type: None,
size: None,
title: None,
meta: None,
}),
" mean?".into(),
],

View File

@@ -428,7 +428,9 @@ mod tests {
use http_client::FakeHttpClient;
use language_model::Role;
use project::Project;
use serde_json::json;
use settings::SettingsStore;
use util::test::TempTree;
fn init_test(cx: &mut TestAppContext) {
env_logger::try_init().ok();
@@ -449,6 +451,8 @@ mod tests {
#[gpui::test]
async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
let tree = TempTree::new(json!({}));
util::paths::set_home_dir(tree.path().into());
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;

View File

@@ -1299,6 +1299,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
status: Some(acp::ToolCallStatus::Completed),
..
},
meta: None,
},
)) if Some(&id) == echo_id.as_ref() => {
echo_completed = true;
@@ -1926,6 +1927,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
acp::PromptRequest {
session_id: session_id.clone(),
prompt: vec!["ghi".into()],
meta: None,
},
cx,
)
@@ -1990,6 +1992,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
locations: vec![],
raw_input: Some(json!({})),
raw_output: None,
meta: None,
}
);
let update = expect_tool_call_update_fields(&mut events).await;
@@ -2003,6 +2006,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_input: Some(json!({ "content": "Thinking hard!" })),
..Default::default()
},
meta: None,
}
);
let update = expect_tool_call_update_fields(&mut events).await;
@@ -2014,6 +2018,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
status: Some(acp::ToolCallStatus::InProgress),
..Default::default()
},
meta: None,
}
);
let update = expect_tool_call_update_fields(&mut events).await;
@@ -2025,6 +2030,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
content: Some(vec!["Thinking hard!".into()]),
..Default::default()
},
meta: None,
}
);
let update = expect_tool_call_update_fields(&mut events).await;
@@ -2037,6 +2043,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_output: Some("Finished thinking.".into()),
..Default::default()
},
meta: None,
}
);
}

View File

@@ -614,6 +614,7 @@ impl Thread {
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
let image = model.map_or(true, |model| model.supports_images());
acp::PromptCapabilities {
meta: None,
image,
audio: false,
embedded_context: true,
@@ -728,6 +729,7 @@ impl Thread {
stream
.0
.unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
meta: None,
id: acp::ToolCallId(tool_use.id.to_string().into()),
title: tool_use.name.to_string(),
kind: acp::ToolKind::Other,
@@ -2333,6 +2335,7 @@ impl ThreadEventStream {
input: serde_json::Value,
) -> acp::ToolCall {
acp::ToolCall {
meta: None,
id: acp::ToolCallId(id.to_string().into()),
title,
kind,
@@ -2352,6 +2355,7 @@ impl ThreadEventStream {
self.0
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
acp::ToolCallUpdate {
meta: None,
id: acp::ToolCallId(tool_use_id.to_string().into()),
fields,
}
@@ -2437,6 +2441,7 @@ impl ToolCallEventStream {
.unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
ToolCallAuthorization {
tool_call: acp::ToolCallUpdate {
meta: None,
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
fields: acp::ToolCallUpdateFields {
title: Some(title.into()),
@@ -2448,16 +2453,19 @@ impl ToolCallEventStream {
id: acp::PermissionOptionId("always_allow".into()),
name: "Always Allow".into(),
kind: acp::PermissionOptionKind::AllowAlways,
meta: None,
},
acp::PermissionOption {
id: acp::PermissionOptionId("allow".into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
meta: None,
},
acp::PermissionOption {
id: acp::PermissionOptionId("deny".into()),
name: "Deny".into(),
kind: acp::PermissionOptionKind::RejectOnce,
meta: None,
},
],
response: response_tx,
@@ -2469,8 +2477,11 @@ impl ToolCallEventStream {
"always_allow" => {
if let Some(fs) = fs.clone() {
cx.update(|cx| {
update_settings_file::<AgentSettings>(fs, cx, |settings, _| {
settings.set_always_allow_tool_actions(true);
update_settings_file(fs, cx, |settings, _| {
settings
.agent
.get_or_insert_default()
.set_always_allow_tool_actions(true);
});
})?;
}
@@ -2611,17 +2622,21 @@ impl From<UserMessageContent> for acp::ContentBlock {
UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent {
data: image.source.to_string(),
mime_type: "image/png".to_string(),
meta: None,
annotations: None,
uri: None,
}),
UserMessageContent::Mention { uri, content } => {
acp::ContentBlock::Resource(acp::EmbeddedResource {
meta: None,
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
meta: None,
mime_type: None,
text: content,
uri: uri.to_uri().to_string(),

View File

@@ -274,6 +274,7 @@ impl AgentTool for EditFileTool {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path,
line: None,
meta: None,
}]),
..Default::default()
});
@@ -353,7 +354,7 @@ impl AgentTool for EditFileTool {
}).ok();
if let Some(abs_path) = abs_path.clone() {
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
locations: Some(vec![ToolCallLocation { path: abs_path, line, meta: None }]),
..Default::default()
});
}
@@ -790,14 +791,11 @@ mod tests {
// First, test with format_on_save enabled
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
cx,
|settings| {
settings.defaults.format_on_save = Some(FormatOnSave::On);
settings.defaults.formatter =
Some(language::language_settings::SelectedFormatter::Auto);
},
);
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
settings.project.all_languages.defaults.formatter =
Some(language::language_settings::SelectedFormatter::Auto);
});
});
});
@@ -852,12 +850,10 @@ mod tests {
// Next, test with format_on_save disabled
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
cx,
|settings| {
settings.defaults.format_on_save = Some(FormatOnSave::Off);
},
);
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.format_on_save =
Some(FormatOnSave::Off);
});
});
});
@@ -934,12 +930,13 @@ mod tests {
// First, test with remove_trailing_whitespace_on_save enabled
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
cx,
|settings| {
settings.defaults.remove_trailing_whitespace_on_save = Some(true);
},
);
store.update_user_settings(cx, |settings| {
settings
.project
.all_languages
.defaults
.remove_trailing_whitespace_on_save = Some(true);
});
});
});
@@ -990,12 +987,13 @@ mod tests {
// Next, test with remove_trailing_whitespace_on_save disabled
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<language::language_settings::AllLanguageSettings>(
cx,
|settings| {
settings.defaults.remove_trailing_whitespace_on_save = Some(false);
},
);
store.update_user_settings(cx, |settings| {
settings
.project
.all_languages
.defaults
.remove_trailing_whitespace_on_save = Some(false);
});
});
});

View File

@@ -138,6 +138,7 @@ impl AgentTool for FindPathTool {
mime_type: None,
size: None,
title: None,
meta: None,
}),
})
.collect(),

View File

@@ -261,10 +261,8 @@ impl AgentTool for GrepTool {
let end_row = range.end.row;
output.push_str("\n### ");
if let Some(parent_symbols) = &parent_symbols {
for symbol in parent_symbols {
write!(output, "{} ", symbol.text)?;
}
for symbol in parent_symbols {
write!(output, "{} ", symbol.text)?;
}
if range.start.row == end_row {
@@ -310,7 +308,7 @@ mod tests {
use super::*;
use gpui::{TestAppContext, UpdateGlobal};
use language::{Language, LanguageConfig, LanguageMatcher};
use project::{FakeFs, Project, WorktreeSettings};
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
use unindent::Unindent;
@@ -829,15 +827,14 @@ mod tests {
cx.update(|cx| {
use gpui::UpdateGlobal;
use project::WorktreeSettings;
use settings::SettingsStore;
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
settings.file_scan_exclusions = Some(vec![
store.update_user_settings(cx, |settings| {
settings.project.worktree.file_scan_exclusions = Some(vec![
"**/.secretdir".to_string(),
"**/.mymetadata".to_string(),
]);
settings.private_files = Some(vec![
settings.project.worktree.private_files = Some(vec![
"**/.mysecrets".to_string(),
"**/*.privatekey".to_string(),
"**/*.mysensitive".to_string(),
@@ -1064,10 +1061,10 @@ mod tests {
// Set global settings
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
settings.file_scan_exclusions =
store.update_user_settings(cx, |settings| {
settings.project.worktree.file_scan_exclusions =
Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
settings.private_files = Some(vec!["**/.env".to_string()]);
settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
});
});
});

View File

@@ -214,7 +214,7 @@ mod tests {
use super::*;
use gpui::{TestAppContext, UpdateGlobal};
use indoc::indoc;
use project::{FakeFs, Project, WorktreeSettings};
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
use util::path;
@@ -421,13 +421,13 @@ mod tests {
// Configure settings explicitly
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
settings.file_scan_exclusions = Some(vec![
store.update_user_settings(cx, |settings| {
settings.project.worktree.file_scan_exclusions = Some(vec![
"**/.secretdir".to_string(),
"**/.mymetadata".to_string(),
"**/.hidden_subdir".to_string(),
]);
settings.private_files = Some(vec![
settings.project.worktree.private_files = Some(vec![
"**/.mysecrets".to_string(),
"**/*.privatekey".to_string(),
"**/*.mysensitive".to_string(),
@@ -565,10 +565,10 @@ mod tests {
// Set global settings
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
settings.file_scan_exclusions =
store.update_user_settings(cx, |settings| {
settings.project.worktree.file_scan_exclusions =
Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
settings.private_files = Some(vec!["**/.env".to_string()]);
settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
});
});
});

View File

@@ -147,8 +147,9 @@ impl AgentTool for ReadFileTool {
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path,
path: abs_path.clone(),
line: input.start_line.map(|line| line.saturating_sub(1)),
meta: None,
}]),
..Default::default()
});
@@ -200,7 +201,6 @@ impl AgentTool for ReadFileTool {
// Check if specific line ranges are provided
let result = if input.start_line.is_some() || input.end_line.is_some() {
let result = buffer.read_with(cx, |buffer, _cx| {
let text = buffer.text();
// .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0.
let start = input.start_line.unwrap_or(1).max(1);
let start_row = start - 1;
@@ -209,13 +209,13 @@ impl AgentTool for ReadFileTool {
anchor = Some(buffer.anchor_before(Point::new(start_row, column)));
}
let lines = text.split('\n').skip(start_row as usize);
if let Some(end) = input.end_line {
let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
itertools::intersperse(lines.take(count as usize), "\n").collect::<String>()
} else {
itertools::intersperse(lines, "\n").collect::<String>()
let mut end_row = input.end_line.unwrap_or(u32::MAX);
if end_row <= start_row {
end_row = start_row + 1; // read at least one lines
}
let start = buffer.anchor_before(Point::new(start_row, 0));
let end = buffer.anchor_before(Point::new(end_row, 0));
buffer.text_for_range(start..end).collect::<String>()
})?;
action_log.update(cx, |log, cx| {
@@ -225,38 +225,30 @@ impl AgentTool for ReadFileTool {
Ok(result.into())
} else {
// No line ranges specified, so check file size to see if it's too big.
let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
let buffer_content =
outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), cx)
.await?;
if file_size <= outline::AUTO_OUTLINE_SIZE {
// File is small enough, so return its contents.
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx);
})?;
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx);
})?;
Ok(result.into())
} else {
// File is too big, so return the outline
// and a suggestion to read again with line numbers.
let outline =
outline::file_outline(project.clone(), file_path, action_log, None, cx)
.await?;
if buffer_content.is_outline {
Ok(formatdoc! {"
This file was too big to read all at once.
Here is an outline of its symbols:
{outline}
{}
Using the line numbers in this outline, you can call this tool again
while specifying the start_line and end_line fields to see the
implementations of symbols in the outline.
Alternatively, you can fall back to the `grep` tool (if available)
to search the file for specific content."
to search the file for specific content.", buffer_content.text
}
.into())
} else {
Ok(buffer_content.text.into())
}
};
@@ -452,7 +444,7 @@ mod test {
tool.run(input, ToolCallEventStream::test().0, cx)
})
.await;
assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4".into());
assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4\n".into());
}
#[gpui::test]
@@ -482,7 +474,7 @@ mod test {
tool.clone().run(input, ToolCallEventStream::test().0, cx)
})
.await;
assert_eq!(result.unwrap(), "Line 1\nLine 2".into());
assert_eq!(result.unwrap(), "Line 1\nLine 2\n".into());
// end_line of 0 should result in at least 1 line
let result = cx
@@ -495,7 +487,7 @@ mod test {
tool.clone().run(input, ToolCallEventStream::test().0, cx)
})
.await;
assert_eq!(result.unwrap(), "Line 1".into());
assert_eq!(result.unwrap(), "Line 1\n".into());
// when start_line > end_line, should still return at least 1 line
let result = cx
@@ -508,7 +500,7 @@ mod test {
tool.clone().run(input, ToolCallEventStream::test().0, cx)
})
.await;
assert_eq!(result.unwrap(), "Line 3".into());
assert_eq!(result.unwrap(), "Line 3\n".into());
}
fn init_test(cx: &mut TestAppContext) {
@@ -594,15 +586,14 @@ mod test {
cx.update(|cx| {
use gpui::UpdateGlobal;
use project::WorktreeSettings;
use settings::SettingsStore;
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
settings.file_scan_exclusions = Some(vec![
store.update_user_settings(cx, |settings| {
settings.project.worktree.file_scan_exclusions = Some(vec![
"**/.secretdir".to_string(),
"**/.mymetadata".to_string(),
]);
settings.private_files = Some(vec![
settings.project.worktree.private_files = Some(vec![
"**/.mysecrets".to_string(),
"**/*.privatekey".to_string(),
"**/*.mysensitive".to_string(),
@@ -810,10 +801,10 @@ mod test {
// Set global settings
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
settings.file_scan_exclusions =
store.update_user_settings(cx, |settings| {
settings.project.worktree.file_scan_exclusions =
Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
settings.private_files = Some(vec!["**/.env".to_string()]);
settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
});
});
});

View File

@@ -122,6 +122,7 @@ fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream)
mime_type: None,
annotations: None,
size: None,
meta: None,
}),
})
.collect(),

View File

@@ -23,6 +23,7 @@ action_log.workspace = true
agent-client-protocol.workspace = true
agent_settings.workspace = true
anyhow.workspace = true
async-trait.workspace = true
client.workspace = true
collections.workspace = true
env_logger = { workspace = true, optional = true }
@@ -30,6 +31,7 @@ fs.workspace = true
futures.workspace = true
gpui.workspace = true
gpui_tokio = { workspace = true, optional = true }
http_client.workspace = true
indoc.workspace = true
language.workspace = true
language_model.workspace = true

View File

@@ -156,9 +156,12 @@ impl AcpConnection {
fs: acp::FileSystemCapability {
read_text_file: true,
write_text_file: true,
meta: None,
},
terminal: true,
meta: None,
},
meta: None,
})
.await?;
@@ -226,6 +229,7 @@ impl AgentConnection for AcpConnection {
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
meta: None,
})
.collect()
} else {
@@ -243,7 +247,7 @@ impl AgentConnection for AcpConnection {
cx.spawn(async move |cx| {
let response = conn
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
.new_session(acp::NewSessionRequest { mcp_servers, cwd, meta: None })
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
@@ -277,6 +281,7 @@ impl AgentConnection for AcpConnection {
let result = conn.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id: default_mode,
meta: None,
})
.await.log_err();
@@ -316,7 +321,7 @@ impl AgentConnection for AcpConnection {
action_log,
session_id.clone(),
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities.clone()),
cx,
)
})?;
@@ -339,13 +344,13 @@ impl AgentConnection for AcpConnection {
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
let result = conn
.authenticate(acp::AuthenticateRequest {
method_id: method_id.clone(),
})
.await?;
conn.authenticate(acp::AuthenticateRequest {
method_id: method_id.clone(),
meta: None,
})
.await?;
Ok(result)
Ok(())
})
}
@@ -396,6 +401,7 @@ impl AgentConnection for AcpConnection {
{
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
meta: None,
})
} else {
Err(anyhow!(details))
@@ -415,6 +421,7 @@ impl AgentConnection for AcpConnection {
let conn = self.connection.clone();
let params = acp::CancelNotification {
session_id: session_id.clone(),
meta: None,
};
cx.foreground_executor()
.spawn(async move { conn.cancel(params).await })
@@ -478,6 +485,7 @@ impl acp_thread::AgentSessionModes for AcpSessionModes {
.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id,
meta: None,
})
.await;
@@ -497,6 +505,7 @@ struct ClientDelegate {
cx: AsyncApp,
}
#[async_trait::async_trait(?Send)]
impl acp::Client for ClientDelegate {
async fn request_permission(
&self,
@@ -526,13 +535,16 @@ impl acp::Client for ClientDelegate {
let outcome = task.await;
Ok(acp::RequestPermissionResponse { outcome })
Ok(acp::RequestPermissionResponse {
outcome,
meta: None,
})
}
async fn write_text_file(
&self,
arguments: acp::WriteTextFileRequest,
) -> Result<(), acp::Error> {
) -> Result<acp::WriteTextFileResponse, acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.session_thread(&arguments.session_id)?
@@ -542,7 +554,7 @@ impl acp::Client for ClientDelegate {
task.await?;
Ok(())
Ok(Default::default())
}
async fn read_text_file(
@@ -558,7 +570,10 @@ impl acp::Client for ClientDelegate {
let content = task.await?;
Ok(acp::ReadTextFileResponse { content })
Ok(acp::ReadTextFileResponse {
content,
meta: None,
})
}
async fn session_notification(
@@ -607,26 +622,41 @@ impl acp::Client for ClientDelegate {
Ok(
terminal.read_with(&self.cx, |terminal, _| acp::CreateTerminalResponse {
terminal_id: terminal.id().clone(),
meta: None,
})?,
)
}
async fn kill_terminal(&self, args: acp::KillTerminalRequest) -> Result<(), acp::Error> {
async fn kill_terminal_command(
&self,
args: acp::KillTerminalCommandRequest,
) -> Result<acp::KillTerminalCommandResponse, acp::Error> {
self.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.kill_terminal(args.terminal_id, cx)
})??;
Ok(())
Ok(Default::default())
}
async fn release_terminal(&self, args: acp::ReleaseTerminalRequest) -> Result<(), acp::Error> {
async fn ext_method(&self, _args: acp::ExtRequest) -> Result<acp::ExtResponse, acp::Error> {
Err(acp::Error::method_not_found())
}
async fn ext_notification(&self, _args: acp::ExtNotification) -> Result<(), acp::Error> {
Err(acp::Error::method_not_found())
}
async fn release_terminal(
&self,
args: acp::ReleaseTerminalRequest,
) -> Result<acp::ReleaseTerminalResponse, acp::Error> {
self.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.release_terminal(args.terminal_id, cx)
})??;
Ok(())
Ok(Default::default())
}
async fn terminal_output(
@@ -655,7 +685,10 @@ impl acp::Client for ClientDelegate {
})??
.await;
Ok(acp::WaitForTerminalExitResponse { exit_status })
Ok(acp::WaitForTerminalExitResponse {
exit_status,
meta: None,
})
}
}

View File

@@ -7,15 +7,19 @@ mod gemini;
pub mod e2e_tests;
pub use claude::*;
use client::ProxySettings;
use collections::HashMap;
pub use custom::*;
use fs::Fs;
pub use gemini::*;
use http_client::read_no_proxy_from_env;
use project::agent_server_store::AgentServerStore;
use acp_thread::AgentConnection;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use gpui::{App, AppContext, Entity, SharedString, Task};
use project::Project;
use settings::SettingsStore;
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
pub use acp::AcpConnection;
@@ -77,3 +81,25 @@ impl dyn AgentServer {
self.into_any().downcast().ok()
}
}
/// Load the default proxy environment variables to pass through to the agent
pub fn load_proxy_env(cx: &mut App) -> HashMap<String, String> {
let proxy_url = cx
.read_global(|settings: &SettingsStore, _| settings.get::<ProxySettings>(None).proxy_url());
let mut env = HashMap::default();
if let Some(proxy_url) = &proxy_url {
let env_var = if proxy_url.scheme() == "https" {
"HTTPS_PROXY"
} else {
"HTTP_PROXY"
};
env.insert(env_var.to_owned(), proxy_url.to_string());
}
if let Some(no_proxy) = read_no_proxy_from_env() {
env.insert("NO_PROXY".to_owned(), no_proxy);
}
env
}

View File

@@ -10,7 +10,7 @@ use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, CLAUDE_CODE_NAME};
use crate::{AgentServer, AgentServerDelegate};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
#[derive(Clone)]
@@ -45,8 +45,13 @@ impl AgentServer for ClaudeCode {
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file::<AllAgentServersSettings>(fs, cx, |settings, _| {
settings.claude.get_or_insert_default().default_mode = mode_id.map(|m| m.to_string())
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.claude
.get_or_insert_default()
.default_mode = mode_id.map(|m| m.to_string())
});
}
@@ -60,6 +65,7 @@ impl AgentServer for ClaudeCode {
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
cx.spawn(async move |cx| {
@@ -70,7 +76,7 @@ impl AgentServer for ClaudeCode {
.context("Claude Code is not registered")?;
anyhow::Ok(agent.get_command(
root_dir.as_deref(),
Default::default(),
extra_env,
delegate.status_tx,
delegate.new_version_available,
&mut cx.to_async(),

View File

@@ -1,4 +1,4 @@
use crate::AgentServerDelegate;
use crate::{AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
@@ -49,8 +49,14 @@ impl crate::AgentServer for CustomAgentServer {
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file::<AllAgentServersSettings>(fs, cx, move |settings, _| {
settings.custom.get_mut(&name).unwrap().default_mode = mode_id.map(|m| m.to_string())
update_settings_file(fs, cx, move |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.custom
.get_mut(&name)
.unwrap()
.default_mode = mode_id.map(|m| m.to_string())
});
}
@@ -65,6 +71,7 @@ impl crate::AgentServer for CustomAgentServer {
let is_remote = delegate.project.read(cx).is_via_remote_server();
let default_mode = self.default_mode(cx);
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
@@ -76,7 +83,7 @@ impl crate::AgentServer for CustomAgentServer {
})?;
anyhow::Ok(agent.get_command(
root_dir.as_deref(),
Default::default(),
extra_env,
delegate.status_tx,
delegate.new_version_available,
&mut cx.to_async(),

View File

@@ -83,6 +83,7 @@ where
acp::ContentBlock::Text(acp::TextContent {
text: "Read the file ".into(),
annotations: None,
meta: None,
}),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: "foo.rs".into(),
@@ -92,10 +93,12 @@ where
mime_type: None,
size: None,
title: None,
meta: None,
}),
acp::ContentBlock::Text(acp::TextContent {
text: " and tell me what the content of the println! is".into(),
annotations: None,
meta: None,
}),
],
cx,

View File

@@ -1,15 +1,12 @@
use std::rc::Rc;
use std::{any::Any, path::Path};
use crate::{AgentServer, AgentServerDelegate};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use anyhow::{Context as _, Result};
use client::ProxySettings;
use collections::HashMap;
use gpui::{App, AppContext, SharedString, Task};
use gpui::{App, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use project::agent_server_store::GEMINI_NAME;
use settings::SettingsStore;
#[derive(Clone)]
pub struct Gemini;
@@ -37,17 +34,20 @@ impl AgentServer for Gemini {
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
let proxy_url = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<ProxySettings>(None).proxy.clone()
});
let mut extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
cx.spawn(async move |cx| {
let mut extra_env = HashMap::default();
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
extra_env.insert("GEMINI_API_KEY".into(), api_key.key);
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
if let Some(api_key) = cx
.update(GoogleLanguageModelProvider::api_key_for_gemini_cli)?
.await
.ok()
{
extra_env.insert("GEMINI_API_KEY".into(), api_key);
}
let (mut command, root_dir, login) = store
let (command, root_dir, login) = store
.update(cx, |store, cx| {
let agent = store
.get_external_agent(&GEMINI_NAME.into())
@@ -62,14 +62,6 @@ impl AgentServer for Gemini {
})??
.await?;
// Add proxy flag if proxy settings are configured in Zed and not in the args
if let Some(proxy_url_value) = &proxy_url
&& !command.args.iter().any(|arg| arg.contains("--proxy"))
{
command.args.push("--proxy".into());
command.args.push(proxy_url_value.clone());
}
let connection = crate::acp::connect(
name,
command,

View File

@@ -1,125 +0,0 @@
use agent_client_protocol as acp;
use std::path::PathBuf;
use crate::AgentServerCommand;
use anyhow::Result;
use collections::HashMap;
use gpui::{App, SharedString};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
pub fn init(cx: &mut App) {
AllAgentServersSettings::register(cx);
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "agent_servers")]
pub struct AllAgentServersSettings {
pub gemini: Option<BuiltinAgentServerSettings>,
pub claude: Option<BuiltinAgentServerSettings>,
/// Custom agent servers configured by the user
#[serde(flatten)]
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct BuiltinAgentServerSettings {
/// Absolute path to a binary to be used when launching this agent.
///
/// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
#[serde(rename = "command")]
pub path: Option<PathBuf>,
/// If a binary is specified in `command`, it will be passed these arguments.
pub args: Option<Vec<String>>,
/// If a binary is specified in `command`, it will be passed these environment variables.
pub env: Option<HashMap<String, String>>,
/// Whether to skip searching `$PATH` for an agent server binary when
/// launching this agent.
///
/// This has no effect if a `command` is specified. Otherwise, when this is
/// `false`, Zed will search `$PATH` for an agent server binary and, if one
/// is found, use it for threads with this agent. If no agent binary is
/// found on `$PATH`, Zed will automatically install and use its own binary.
/// When this is `true`, Zed will not search `$PATH`, and will always use
/// its own binary.
///
/// Default: true
pub ignore_system_version: Option<bool>,
/// The default mode for new threads.
///
/// Note: Not all agents support modes.
///
/// Default: None
#[serde(skip_serializing_if = "Option::is_none")]
pub default_mode: Option<acp::SessionModeId>,
}
impl BuiltinAgentServerSettings {
pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
self.path.map(|path| AgentServerCommand {
path,
args: self.args.unwrap_or_default(),
env: self.env,
})
}
}
impl From<AgentServerCommand> for BuiltinAgentServerSettings {
fn from(value: AgentServerCommand) -> Self {
BuiltinAgentServerSettings {
path: Some(value.path),
args: Some(value.args),
env: value.env,
..Default::default()
}
}
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct CustomAgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
/// The default mode for new threads.
///
/// Note: Not all agents support modes.
///
/// Default: None
#[serde(skip_serializing_if = "Option::is_none")]
pub default_mode: Option<acp::SessionModeId>,
}
impl settings::Settings for AllAgentServersSettings {
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
for AllAgentServersSettings {
gemini,
claude,
custom,
} in sources.defaults_and_customizations()
{
if gemini.is_some() {
settings.gemini = gemini.clone();
}
if claude.is_some() {
settings.claude = claude.clone();
}
// Merge custom agents
for (name, config) in custom {
// Skip built-in agent names to avoid conflicts
if name != "gemini" && name != "claude" {
settings.custom.insert(name.clone(), config.clone());
}
}
}
Ok(settings)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -15,11 +15,14 @@ path = "src/agent_settings.rs"
anyhow.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
convert_case.workspace = true
fs.workspace = true
gpui.workspace = true
language_model.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
util.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

View File

@@ -1,9 +1,17 @@
use std::sync::Arc;
use anyhow::{Result, bail};
use collections::IndexMap;
use gpui::SharedString;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use convert_case::{Case, Casing as _};
use fs::Fs;
use gpui::{App, SharedString};
use settings::{
AgentProfileContent, ContextServerPresetContent, Settings as _, SettingsContent,
update_settings_file,
};
use util::ResultExt as _;
use crate::{AgentProfileId, AgentSettings};
pub mod builtin_profiles {
use super::AgentProfileId;
@@ -17,24 +25,66 @@ pub mod builtin_profiles {
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AgentProfileId(pub Arc<str>);
impl AgentProfileId {
pub fn as_str(&self) -> &str {
&self.0
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentProfile {
id: AgentProfileId,
}
impl std::fmt::Display for AgentProfileId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
pub type AvailableProfiles = IndexMap<AgentProfileId, SharedString>;
impl Default for AgentProfileId {
fn default() -> Self {
Self("write".into())
impl AgentProfile {
pub fn new(id: AgentProfileId) -> Self {
Self { id }
}
pub fn id(&self) -> &AgentProfileId {
&self.id
}
/// Saves a new profile to the settings.
pub fn create(
name: String,
base_profile_id: Option<AgentProfileId>,
fs: Arc<dyn Fs>,
cx: &App,
) -> AgentProfileId {
let id = AgentProfileId(name.to_case(Case::Kebab).into());
let base_profile =
base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned());
let profile_settings = AgentProfileSettings {
name: name.into(),
tools: base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default(),
enable_all_context_servers: base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default(),
context_servers: base_profile
.map(|profile| profile.context_servers)
.unwrap_or_default(),
};
update_settings_file(fs, cx, {
let id = id.clone();
move |settings, _cx| {
profile_settings.save_to_settings(id, settings).log_err();
}
});
id
}
/// Returns a map of AgentProfileIds to their names
pub fn available_profiles(cx: &App) -> AvailableProfiles {
let mut profiles = AvailableProfiles::default();
for (id, profile) in AgentSettings::get_global(cx).profiles.iter() {
profiles.insert(id.clone(), profile.name.clone());
}
profiles
}
}
@@ -60,9 +110,71 @@ impl AgentProfileSettings {
.get(server_id)
.is_some_and(|preset| preset.tools.get(tool_name) == Some(&true))
}
pub fn save_to_settings(
&self,
profile_id: AgentProfileId,
content: &mut SettingsContent,
) -> Result<()> {
let profiles = content
.agent
.get_or_insert_default()
.profiles
.get_or_insert_default();
if profiles.contains_key(&profile_id.0) {
bail!("profile with ID '{profile_id}' already exists");
}
profiles.insert(
profile_id.0,
AgentProfileContent {
name: self.name.clone().into(),
tools: self.tools.clone(),
enable_all_context_servers: Some(self.enable_all_context_servers),
context_servers: self
.context_servers
.clone()
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
},
);
Ok(())
}
}
impl From<AgentProfileContent> for AgentProfileSettings {
fn from(content: AgentProfileContent) -> Self {
Self {
name: content.name.into(),
tools: content.tools,
enable_all_context_servers: content.enable_all_context_servers.unwrap_or_default(),
context_servers: content
.context_servers
.into_iter()
.map(|(server_id, preset)| (server_id, preset.into()))
.collect(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ContextServerPreset {
pub tools: IndexMap<Arc<str>, bool>,
}
impl From<settings::ContextServerPresetContent> for ContextServerPreset {
fn from(content: settings::ContextServerPresetContent) -> Self {
Self {
tools: content.tools,
}
}
}

View File

@@ -2,14 +2,16 @@ mod agent_profile;
use std::sync::Arc;
use anyhow::{Result, bail};
use collections::IndexMap;
use gpui::{App, Pixels, SharedString};
use gpui::{App, Pixels, px};
use language_model::LanguageModel;
use schemars::{JsonSchema, json_schema};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use std::borrow::Cow;
use settings::{
DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
NotifyWhenAgentWaiting, Settings, SettingsContent,
};
use util::MergeFrom;
pub use crate::agent_profile::*;
@@ -22,37 +24,11 @@ pub fn init(cx: &mut App) {
AgentSettings::register(cx);
}
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AgentDockPosition {
Left,
#[default]
Right,
Bottom,
}
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DefaultView {
#[default]
Thread,
TextThread,
}
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NotifyWhenAgentWaiting {
#[default]
PrimaryScreen,
AllScreens,
Never,
}
#[derive(Default, Clone, Debug)]
#[derive(Clone, Debug)]
pub struct AgentSettings {
pub enabled: bool,
pub button: bool,
pub dock: AgentDockPosition,
pub dock: DockPosition,
pub default_width: Pixels,
pub default_height: Pixels,
pub default_model: Option<LanguageModelSelection>,
@@ -60,9 +36,8 @@ pub struct AgentSettings {
pub commit_message_model: Option<LanguageModelSelection>,
pub thread_summary_model: Option<LanguageModelSelection>,
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub default_profile: AgentProfileId,
pub default_view: DefaultView,
pub default_view: DefaultAgentView,
pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
pub always_allow_tool_actions: bool,
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
@@ -75,76 +50,26 @@ pub struct AgentSettings {
pub expand_edit_card: bool,
pub expand_terminal_card: bool,
pub use_modifier_to_send: bool,
pub message_editor_min_lines: usize,
}
impl AgentSettings {
pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {
let settings = Self::get_global(cx);
settings
.model_parameters
.iter()
.rfind(|setting| setting.matches(model))
.and_then(|m| m.temperature)
}
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
self.inline_assistant_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
}
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
self.commit_message_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
}
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
self.thread_summary_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct LanguageModelParameters {
pub provider: Option<LanguageModelProviderSetting>,
pub model: Option<SharedString>,
pub temperature: Option<f32>,
}
impl LanguageModelParameters {
pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool {
if let Some(provider) = &self.provider
&& provider.0 != model.provider_id().0
{
return false;
for setting in settings.model_parameters.iter().rev() {
if let Some(provider) = &setting.provider
&& provider.0 != model.provider_id().0
{
continue;
}
if let Some(setting_model) = &setting.model
&& *setting_model != model.id().0
{
continue;
}
return setting.temperature;
}
if let Some(setting_model) = &self.model
&& *setting_model != model.id().0
{
return false;
}
true
}
}
impl AgentSettingsContent {
pub fn set_dock(&mut self, dock: AgentDockPosition) {
self.dock = Some(dock);
}
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
let model = language_model.id().0.to_string();
let provider = language_model.provider_id().0.to_string();
self.default_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
return None;
}
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
@@ -168,158 +93,9 @@ impl AgentSettingsContent {
});
}
pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
self.always_allow_tool_actions = Some(allow);
pub fn set_message_editor_max_lines(&self) -> usize {
self.message_editor_min_lines * 2
}
pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
self.play_sound_when_agent_done = Some(allow);
}
pub fn set_single_file_review(&mut self, allow: bool) {
self.single_file_review = Some(allow);
}
pub fn set_use_modifier_to_send(&mut self, always_use: bool) {
self.use_modifier_to_send = Some(always_use);
}
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
self.default_profile = Some(profile_id);
}
pub fn create_profile(
&mut self,
profile_id: AgentProfileId,
profile_settings: AgentProfileSettings,
) -> Result<()> {
let profiles = self.profiles.get_or_insert_default();
if profiles.contains_key(&profile_id) {
bail!("profile with ID '{profile_id}' already exists");
}
profiles.insert(
profile_id,
AgentProfileContent {
name: profile_settings.name.into(),
tools: profile_settings.tools,
enable_all_context_servers: Some(profile_settings.enable_all_context_servers),
context_servers: profile_settings
.context_servers
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
},
);
Ok(())
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
#[settings_key(key = "agent", fallback_key = "assistant")]
pub struct AgentSettingsContent {
/// Whether the Agent is enabled.
///
/// Default: true
enabled: Option<bool>,
/// Whether to show the agent panel button in the status bar.
///
/// Default: true
button: Option<bool>,
/// Where to dock the agent panel.
///
/// Default: right
dock: Option<AgentDockPosition>,
/// Default width in pixels when the agent panel is docked to the left or right.
///
/// Default: 640
default_width: Option<f32>,
/// Default height in pixels when the agent panel is docked to the bottom.
///
/// Default: 320
default_height: Option<f32>,
/// The default model to use when creating new chats and for other features when a specific model is not specified.
default_model: Option<LanguageModelSelection>,
/// Model to use for the inline assistant. Defaults to default_model when not specified.
inline_assistant_model: Option<LanguageModelSelection>,
/// Model to use for generating git commit messages. Defaults to default_model when not specified.
commit_message_model: Option<LanguageModelSelection>,
/// Model to use for generating thread summaries. Defaults to default_model when not specified.
thread_summary_model: Option<LanguageModelSelection>,
/// Additional models with which to generate alternatives when performing inline assists.
inline_alternatives: Option<Vec<LanguageModelSelection>>,
/// The default profile to use in the Agent.
///
/// Default: write
default_profile: Option<AgentProfileId>,
/// Which view type to show by default in the agent panel.
///
/// Default: "thread"
default_view: Option<DefaultView>,
/// The available agent profiles.
pub profiles: Option<IndexMap<AgentProfileId, AgentProfileContent>>,
/// Whenever a tool action would normally wait for your confirmation
/// that you allow it, always choose to allow it.
///
/// This setting has no effect on external agents that support permission modes, such as Claude Code.
///
/// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code.
///
/// Default: false
always_allow_tool_actions: Option<bool>,
/// Where to show a popup notification when the agent is waiting for user input.
///
/// Default: "primary_screen"
notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
/// Whether to play a sound when the agent has either completed its response, or needs user input.
///
/// Default: false
play_sound_when_agent_done: Option<bool>,
/// Whether to stream edits from the agent as they are received.
///
/// Default: false
stream_edits: Option<bool>,
/// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
///
/// Default: true
single_file_review: Option<bool>,
/// Additional parameters for language model requests. When making a request
/// to a model, parameters will be taken from the last entry in this list
/// that matches the model's provider and name. In each entry, both provider
/// and model are optional, so that you can specify parameters for either
/// one.
///
/// Default: []
#[serde(default)]
model_parameters: Vec<LanguageModelParameters>,
/// What completion mode to enable for new threads
///
/// Default: normal
preferred_completion_mode: Option<CompletionMode>,
/// Whether to show thumb buttons for feedback in the agent panel.
///
/// Default: true
enable_feedback: Option<bool>,
/// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
///
/// Default: true
expand_edit_card: Option<bool>,
/// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
///
/// Default: true
expand_terminal_card: Option<bool>,
/// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
///
/// Default: false
use_modifier_to_send: Option<bool>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
@@ -340,202 +116,140 @@ impl From<CompletionMode> for cloud_llm_client::CompletionMode {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct LanguageModelSelection {
pub provider: LanguageModelProviderSetting,
pub model: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct LanguageModelProviderSetting(pub String);
impl JsonSchema for LanguageModelProviderSetting {
fn schema_name() -> Cow<'static, str> {
"LanguageModelProviderSetting".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"enum": [
"amazon-bedrock",
"anthropic",
"copilot_chat",
"deepseek",
"google",
"lmstudio",
"mistral",
"ollama",
"openai",
"openrouter",
"vercel",
"x_ai",
"zed.dev"
]
})
impl From<settings::CompletionMode> for CompletionMode {
fn from(value: settings::CompletionMode) -> Self {
match value {
settings::CompletionMode::Normal => CompletionMode::Normal,
settings::CompletionMode::Burn => CompletionMode::Burn,
}
}
}
impl From<String> for LanguageModelProviderSetting {
fn from(provider: String) -> Self {
Self(provider)
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AgentProfileId(pub Arc<str>);
impl AgentProfileId {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for LanguageModelProviderSetting {
fn from(provider: &str) -> Self {
Self(provider.to_string())
impl std::fmt::Display for AgentProfileId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AgentProfileContent {
pub name: Arc<str>,
#[serde(default)]
pub tools: IndexMap<Arc<str>, bool>,
/// Whether all context servers are enabled by default.
pub enable_all_context_servers: Option<bool>,
#[serde(default)]
pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
}
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ContextServerPresetContent {
pub tools: IndexMap<Arc<str>, bool>,
impl Default for AgentProfileId {
fn default() -> Self {
Self("write".into())
}
}
impl Settings for AgentSettings {
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
type FileContent = AgentSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::App,
) -> anyhow::Result<Self> {
let mut settings = AgentSettings::default();
for value in sources.defaults_and_customizations() {
merge(&mut settings.enabled, value.enabled);
merge(&mut settings.button, value.button);
merge(&mut settings.dock, value.dock);
merge(
&mut settings.default_width,
value.default_width.map(Into::into),
);
merge(
&mut settings.default_height,
value.default_height.map(Into::into),
);
settings.default_model = value
.default_model
.clone()
.or(settings.default_model.take());
settings.inline_assistant_model = value
.inline_assistant_model
.clone()
.or(settings.inline_assistant_model.take());
settings.commit_message_model = value
.clone()
.commit_message_model
.or(settings.commit_message_model.take());
settings.thread_summary_model = value
.clone()
.thread_summary_model
.or(settings.thread_summary_model.take());
merge(
&mut settings.inline_alternatives,
value.inline_alternatives.clone(),
);
merge(
&mut settings.notify_when_agent_waiting,
value.notify_when_agent_waiting,
);
merge(
&mut settings.play_sound_when_agent_done,
value.play_sound_when_agent_done,
);
merge(&mut settings.stream_edits, value.stream_edits);
merge(&mut settings.single_file_review, value.single_file_review);
merge(&mut settings.default_profile, value.default_profile.clone());
merge(&mut settings.default_view, value.default_view);
merge(
&mut settings.preferred_completion_mode,
value.preferred_completion_mode,
);
merge(&mut settings.enable_feedback, value.enable_feedback);
merge(&mut settings.expand_edit_card, value.expand_edit_card);
merge(
&mut settings.expand_terminal_card,
value.expand_terminal_card,
);
merge(
&mut settings.use_modifier_to_send,
value.use_modifier_to_send,
);
settings
.model_parameters
.extend_from_slice(&value.model_parameters);
if let Some(profiles) = value.profiles.as_ref() {
settings
.profiles
.extend(profiles.into_iter().map(|(id, profile)| {
(
id.clone(),
AgentProfileSettings {
name: profile.name.clone().into(),
tools: profile.tools.clone(),
enable_all_context_servers: profile
.enable_all_context_servers
.unwrap_or_default(),
context_servers: profile
.context_servers
.iter()
.map(|(context_server_id, preset)| {
(
context_server_id.clone(),
ContextServerPreset {
tools: preset.tools.clone(),
},
)
})
.collect(),
},
)
}));
}
fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
let agent = content.agent.clone().unwrap();
Self {
enabled: agent.enabled.unwrap(),
button: agent.button.unwrap(),
dock: agent.dock.unwrap(),
default_width: px(agent.default_width.unwrap()),
default_height: px(agent.default_height.unwrap()),
default_model: Some(agent.default_model.unwrap()),
inline_assistant_model: agent.inline_assistant_model,
commit_message_model: agent.commit_message_model,
thread_summary_model: agent.thread_summary_model,
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
default_profile: AgentProfileId(agent.default_profile.unwrap()),
default_view: agent.default_view.unwrap(),
profiles: agent
.profiles
.unwrap()
.into_iter()
.map(|(key, val)| (AgentProfileId(key), val.into()))
.collect(),
always_allow_tool_actions: agent.always_allow_tool_actions.unwrap(),
notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(),
play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(),
stream_edits: agent.stream_edits.unwrap(),
single_file_review: agent.single_file_review.unwrap(),
model_parameters: agent.model_parameters,
preferred_completion_mode: agent.preferred_completion_mode.unwrap().into(),
enable_feedback: agent.enable_feedback.unwrap(),
expand_edit_card: agent.expand_edit_card.unwrap(),
expand_terminal_card: agent.expand_terminal_card.unwrap(),
use_modifier_to_send: agent.use_modifier_to_send.unwrap(),
message_editor_min_lines: agent.message_editor_min_lines.unwrap(),
}
debug_assert!(
!sources.default.always_allow_tool_actions.unwrap_or(false),
"For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!"
);
// For security reasons, only trust the user's global settings for whether to always allow tool actions.
// If this could be overridden locally, an attacker could (e.g. by committing to source control and
// convincing you to switch branches) modify your project-local settings to disable the agent's safety checks.
settings.always_allow_tool_actions = sources
.user
.and_then(|setting| setting.always_allow_tool_actions)
.unwrap_or(false);
Ok(settings)
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) {
let Some(value) = &content.agent else { return };
self.enabled.merge_from(&value.enabled);
self.button.merge_from(&value.button);
self.dock.merge_from(&value.dock);
self.default_width
.merge_from(&value.default_width.map(Into::into));
self.default_height
.merge_from(&value.default_height.map(Into::into));
self.default_model = value.default_model.clone().or(self.default_model.take());
self.inline_assistant_model = value
.inline_assistant_model
.clone()
.or(self.inline_assistant_model.take());
self.commit_message_model = value
.clone()
.commit_message_model
.or(self.commit_message_model.take());
self.thread_summary_model = value
.clone()
.thread_summary_model
.or(self.thread_summary_model.take());
self.inline_alternatives
.merge_from(&value.inline_alternatives.clone());
self.default_profile
.merge_from(&value.default_profile.clone().map(AgentProfileId));
self.default_view.merge_from(&value.default_view);
self.always_allow_tool_actions
.merge_from(&value.always_allow_tool_actions);
self.notify_when_agent_waiting
.merge_from(&value.notify_when_agent_waiting);
self.play_sound_when_agent_done
.merge_from(&value.play_sound_when_agent_done);
self.stream_edits.merge_from(&value.stream_edits);
self.single_file_review
.merge_from(&value.single_file_review);
self.preferred_completion_mode
.merge_from(&value.preferred_completion_mode.map(Into::into));
self.enable_feedback.merge_from(&value.enable_feedback);
self.expand_edit_card.merge_from(&value.expand_edit_card);
self.expand_terminal_card
.merge_from(&value.expand_terminal_card);
self.use_modifier_to_send
.merge_from(&value.use_modifier_to_send);
self.model_parameters
.extend_from_slice(&value.model_parameters);
self.message_editor_min_lines
.merge_from(&value.message_editor_min_lines);
if let Some(profiles) = value.profiles.as_ref() {
self.profiles.extend(
profiles
.into_iter()
.map(|(id, profile)| (AgentProfileId(id.clone()), profile.clone().into())),
);
}
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
if let Some(b) = vscode
.read_value("chat.agent.enabled")
.and_then(|b| b.as_bool())
{
current.enabled = Some(b);
current.button = Some(b);
current.agent.get_or_insert_default().enabled = Some(b);
current.agent.get_or_insert_default().button = Some(b);
}
}
}
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
}

View File

@@ -52,7 +52,6 @@ gpui.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indoc.workspace = true
inventory.workspace = true
itertools.workspace = true
jsonschema.workspace = true
language.workspace = true
@@ -81,7 +80,6 @@ serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
shlex.workspace = true
smol.workspace = true
streaming_diff.workspace = true
task.workspace = true
@@ -98,7 +96,6 @@ ui_input.workspace = true
url.workspace = true
urlencoding.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
workspace-hack.workspace = true
workspace.workspace = true

View File

@@ -1,4 +1,4 @@
use std::cell::{Cell, RefCell};
use std::cell::RefCell;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
@@ -68,7 +68,7 @@ pub struct ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
}
@@ -78,7 +78,7 @@ impl ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
) -> Self {
Self {
@@ -600,7 +600,7 @@ impl ContextPickerCompletionProvider {
}),
);
if self.prompt_capabilities.get().embedded_context {
if self.prompt_capabilities.borrow().embedded_context {
const RECENT_COUNT: usize = 2;
let threads = self
.history_store
@@ -622,7 +622,7 @@ impl ContextPickerCompletionProvider {
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<ContextPickerEntry> {
let embedded_context = self.prompt_capabilities.get().embedded_context;
let embedded_context = self.prompt_capabilities.borrow().embedded_context;
let mut entries = if embedded_context {
vec![
ContextPickerEntry::Mode(ContextPickerMode::File),
@@ -694,7 +694,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
ContextCompletion::try_parse(
line,
offset_to_line,
self.prompt_capabilities.get().embedded_context,
self.prompt_capabilities.borrow().embedded_context,
)
});
let Some(state) = state else {
@@ -896,7 +896,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
ContextCompletion::try_parse(
line,
offset_to_line,
self.prompt_capabilities.get().embedded_context,
self.prompt_capabilities.borrow().embedded_context,
)
.map(|completion| {
completion.source_range().start <= offset_to_line + position.column as usize
@@ -1108,6 +1108,12 @@ impl MentionCompletion {
match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
Some(whitespace_count) => {
if let Some(argument_text) = parts.next() {
// If mode wasn't recognized but we have an argument, don't suggest completions
// (e.g. '@something word')
if mode.is_none() && !argument_text.is_empty() {
return None;
}
argument = Some(argument_text.to_string());
end += whitespace_count + argument_text.len();
}
@@ -1265,6 +1271,17 @@ mod tests {
})
);
assert_eq!(
MentionCompletion::try_parse(true, "Lorem @main ", 0),
Some(MentionCompletion {
source_range: 6..12,
mode: None,
argument: Some("main".to_string()),
})
);
assert_eq!(MentionCompletion::try_parse(true, "Lorem @main m", 0), None);
assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None);
// Allowed non-file mentions
@@ -1279,14 +1296,9 @@ mod tests {
);
// Disallowed non-file mentions
assert_eq!(
MentionCompletion::try_parse(false, "Lorem @symbol main", 0),
Some(MentionCompletion {
source_range: 6..18,
mode: None,
argument: Some("main".to_string()),
})
None
);
assert_eq!(

View File

@@ -1,8 +1,4 @@
use std::{
cell::{Cell, RefCell},
ops::Range,
rc::Rc,
};
use std::{cell::RefCell, ops::Range, rc::Rc};
use acp_thread::{AcpThread, AgentThreadEntry};
use agent_client_protocol::{self as acp, ToolCallId};
@@ -30,7 +26,7 @@ pub struct EntryViewState {
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
entries: Vec<Entry>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
}
@@ -41,7 +37,7 @@ impl EntryViewState {
project: Entity<Project>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
) -> Self {
@@ -448,11 +444,13 @@ mod tests {
path: "/project/hello.txt".into(),
old_text: Some("hi world".into()),
new_text: "hello world".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
};
let connection = Rc::new(StubAgentConnection::new());
let thread = cx

View File

@@ -8,6 +8,7 @@ use agent_servers::{AgentServer, AgentServerDelegate};
use agent2::HistoryStore;
use anyhow::{Result, anyhow};
use assistant_slash_commands::codeblock_fence_for_path;
use assistant_tool::outline;
use collections::{HashMap, HashSet};
use editor::{
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
@@ -35,7 +36,7 @@ use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::Settings;
use std::{
cell::{Cell, RefCell},
cell::RefCell,
ffi::OsStr,
fmt::Write,
ops::{Range, RangeInclusive},
@@ -63,7 +64,7 @@ pub struct MessageEditor {
workspace: WeakEntity<Workspace>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
_subscriptions: Vec<Subscription>,
@@ -88,10 +89,10 @@ impl MessageEditor {
project: Entity<Project>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
placeholder: impl Into<Arc<str>>,
placeholder: &str,
mode: EditorMode,
window: &mut Window,
cx: &mut Context<Self>,
@@ -117,7 +118,7 @@ impl MessageEditor {
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let mut editor = Editor::new(mode, buffer, None, window, cx);
editor.set_placeholder_text(placeholder, cx);
editor.set_placeholder_text(placeholder, window, cx);
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap();
editor.set_use_modal_editing(true);
@@ -427,7 +428,7 @@ impl MessageEditor {
.unwrap_or_default();
if Img::extensions().contains(&extension) && !extension.contains("svg") {
if !self.prompt_capabilities.get().image {
if !self.prompt_capabilities.borrow().image {
return Task::ready(Err(anyhow!("This model does not support images yet")));
}
let task = self
@@ -456,11 +457,14 @@ impl MessageEditor {
.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(async move |_, cx| {
let buffer = buffer.await?;
let mention = buffer.update(cx, |buffer, cx| Mention::Text {
content: buffer.text(),
tracked_buffers: vec![cx.entity()],
})?;
anyhow::Ok(mention)
let buffer_content =
outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), &cx)
.await?;
Ok(Mention::Text {
content: buffer_content.text,
tracked_buffers: vec![buffer],
})
})
}
@@ -520,18 +524,17 @@ impl MessageEditor {
})
});
// TODO: report load errors instead of just logging
let rope_task = cx.spawn(async move |cx| {
cx.spawn(async move |cx| {
let buffer = open_task.await.log_err()?;
let rope = buffer
.read_with(cx, |buffer, _cx| buffer.as_rope().clone())
.log_err()?;
Some((rope, buffer))
});
let buffer_content = outline::get_buffer_content_or_outline(
buffer.clone(),
Some(&full_path),
&cx,
)
.await
.ok()?;
cx.background_spawn(async move {
let (rope, buffer) = rope_task.await?;
Some((rel_path, full_path, rope.to_string(), buffer))
Some((rel_path, full_path, buffer_content.text, buffer))
})
}))
})?;
@@ -786,7 +789,7 @@ impl MessageEditor {
let contents = self
.mention_set
.contents(&self.prompt_capabilities.get(), cx);
.contents(&self.prompt_capabilities.borrow(), cx);
let editor = self.editor.clone();
cx.spawn(async move |_, cx| {
@@ -831,8 +834,10 @@ impl MessageEditor {
mime_type: None,
text: content.clone(),
uri: uri.to_uri().to_string(),
meta: None,
},
),
meta: None,
})
}
Mention::Image(mention_image) => {
@@ -852,6 +857,7 @@ impl MessageEditor {
data: mention_image.data.to_string(),
mime_type: mention_image.format.mime_type().into(),
uri,
meta: None,
})
}
Mention::UriOnly => {
@@ -863,6 +869,7 @@ impl MessageEditor {
mime_type: None,
size: None,
title: None,
meta: None,
})
}
};
@@ -917,7 +924,7 @@ impl MessageEditor {
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if !self.prompt_capabilities.get().image {
if !self.prompt_capabilities.borrow().image {
return;
}
@@ -1092,11 +1099,16 @@ impl MessageEditor {
}
pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let buffer = self.editor.read(cx).buffer().clone();
let Some(buffer) = buffer.read(cx).as_singleton() else {
let editor = self.editor.read(cx);
let editor_buffer = editor.buffer().read(cx);
let Some(buffer) = editor_buffer.as_singleton() else {
return;
};
let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
let cursor_anchor = editor.selections.newest_anchor().head();
let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
let anchor = buffer.update(cx, |buffer, _cx| {
buffer.anchor_before(cursor_offset.min(buffer.len()))
});
let Some(workspace) = self.workspace.upgrade() else {
return;
};
@@ -1110,13 +1122,7 @@ impl MessageEditor {
return;
};
self.editor.update(cx, |message_editor, cx| {
message_editor.edit(
[(
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
completion.new_text,
)],
cx,
);
message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
});
if let Some(confirm) = completion.confirm {
confirm(CompletionIntent::Complete, window, cx);
@@ -1185,6 +1191,7 @@ impl MessageEditor {
data,
mime_type,
annotations: _,
meta: _,
}) => {
let mention_uri = if let Some(uri) = uri {
MentionUri::parse(&uri)
@@ -1568,18 +1575,13 @@ impl Addon for MessageEditorAddon {
#[cfg(test)]
mod tests {
use std::{
cell::{Cell, RefCell},
ops::Range,
path::Path,
rc::Rc,
sync::Arc,
};
use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
use acp_thread::MentionUri;
use agent_client_protocol as acp;
use agent2::HistoryStore;
use assistant_context::ContextStore;
use assistant_tool::outline;
use editor::{AnchorRangeExt as _, Editor, EditorMode};
use fs::FakeFs;
use futures::StreamExt as _;
@@ -1720,7 +1722,7 @@ mod tests {
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
// Start with no available commands - simulating Claude which doesn't support slash commands
let available_commands = Rc::new(RefCell::new(vec![]));
@@ -1769,6 +1771,7 @@ mod tests {
name: "help".to_string(),
description: "Get help".to_string(),
input: None,
meta: None,
}]);
// Test that unsupported slash commands trigger an error when we have a list of available commands
@@ -1883,12 +1886,13 @@ mod tests {
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let available_commands = Rc::new(RefCell::new(vec![
acp::AvailableCommand {
name: "quick-math".to_string(),
description: "2 + 2 = 4 - 1 = 3".to_string(),
input: None,
meta: None,
},
acp::AvailableCommand {
name: "say-hello".to_string(),
@@ -1896,6 +1900,7 @@ mod tests {
input: Some(acp::AvailableCommandInput::Unstructured {
hint: "<name>".to_string(),
}),
meta: None,
},
]));
@@ -2130,7 +2135,7 @@ mod tests {
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
@@ -2185,10 +2190,11 @@ mod tests {
editor.set_text("", window, cx);
});
prompt_capabilities.set(acp::PromptCapabilities {
prompt_capabilities.replace(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
});
cx.simulate_input("Lorem ");
@@ -2260,6 +2266,7 @@ mod tests {
image: true,
audio: true,
embedded_context: true,
meta: None,
};
let contents = message_editor
@@ -2584,4 +2591,110 @@ mod tests {
})
.collect::<Vec<_>>()
}
#[gpui::test]
async fn test_large_file_mention_uses_outline(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
// Create a large file that exceeds AUTO_OUTLINE_SIZE
const LINE: &str = "fn example_function() { /* some code */ }\n";
let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
// Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
let small_content = "fn small_function() { /* small */ }\n";
assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
fs.insert_tree(
"/project",
json!({
"large_file.rs": large_content.clone(),
"small_file.rs": small_content,
}),
)
.await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
let message_editor = cx.update(|window, cx| {
cx.new(|cx| {
let editor = MessageEditor::new(
workspace.downgrade(),
project.clone(),
history_store.clone(),
None,
Default::default(),
Default::default(),
"Test Agent".into(),
"Test",
EditorMode::AutoHeight {
min_lines: 1,
max_lines: None,
},
window,
cx,
);
// Enable embedded context so files are actually included
editor.prompt_capabilities.replace(acp::PromptCapabilities {
embedded_context: true,
meta: None,
..Default::default()
});
editor
})
});
// Test large file mention
// Get the absolute path using the project's worktree
let large_file_abs_path = project.read_with(cx, |project, cx| {
let worktree = project.worktrees(cx).next().unwrap();
let worktree_root = worktree.read(cx).abs_path();
worktree_root.join("large_file.rs")
});
let large_file_task = message_editor.update(cx, |editor, cx| {
editor.confirm_mention_for_file(large_file_abs_path, cx)
});
let large_file_mention = large_file_task.await.unwrap();
match large_file_mention {
Mention::Text { content, .. } => {
// Should contain outline header for large files
assert!(content.contains("File outline for"));
assert!(content.contains("file too large to show full content"));
// Should not contain the full repeated content
assert!(!content.contains(&LINE.repeat(100)));
}
_ => panic!("Expected Text mention for large file"),
}
// Test small file mention
// Get the absolute path using the project's worktree
let small_file_abs_path = project.read_with(cx, |project, cx| {
let worktree = project.worktrees(cx).next().unwrap();
let worktree_root = worktree.read(cx).abs_path();
worktree_root.join("small_file.rs")
});
let small_file_task = message_editor.update(cx, |editor, cx| {
editor.confirm_mention_for_file(small_file_abs_path, cx)
});
let small_file_mention = small_file_task.await.unwrap();
match small_file_mention {
Mention::Text { content, .. } => {
// Should contain the actual content
assert_eq!(content, small_content);
// Should not contain outline header
assert!(!content.contains("File outline for"));
}
_ => panic!("Expected Text mention for small file"),
}
}
}

View File

@@ -5,8 +5,8 @@ use fs::Fs;
use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
use std::{rc::Rc, sync::Arc};
use ui::{
Button, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding,
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
};
use crate::{CycleModeSelector, ToggleProfileSelector};
@@ -91,7 +91,7 @@ impl ModeSelector {
.toggleable(IconPosition::End, is_selected);
let entry = if let Some(description) = &mode.description {
entry.documentation_aside(ui::DocumentationSide::Left, {
entry.documentation_aside(DocumentationSide::Left, DocumentationEdge::Bottom, {
let description = description.clone();
move |cx| {
@@ -107,13 +107,15 @@ impl ModeSelector {
.text_sm()
.text_color(Color::Muted.color(cx))
.child("Hold")
.child(div().pt_0p5().children(ui::render_modifiers(
&gpui::Modifiers::secondary_key(),
PlatformStyle::platform(),
None,
Some(ui::TextSize::Default.rems(cx).into()),
true,
)))
.child(h_flex().flex_shrink_0().children(
ui::render_modifiers(
&gpui::Modifiers::secondary_key(),
PlatformStyle::platform(),
None,
Some(ui::TextSize::Default.rems(cx).into()),
true,
),
))
.child(div().map(|this| {
if is_default {
this.child("to also unset as default")
@@ -223,6 +225,10 @@ impl Render for ModeSelector {
)
.anchor(gpui::Corner::BottomRight)
.with_handle(self.menu_handle.clone())
.offset(gpui::Point {
x: px(0.0),
y: px(-2.0),
})
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})

View File

@@ -5,15 +5,15 @@ use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate;
use gpui::{
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
UniformListScrollHandle, WeakEntity, Window, uniform_list,
};
use std::{fmt::Display, ops::Range};
use text::Bias;
use time::{OffsetDateTime, UtcOffset};
use ui::{
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
Tooltip, prelude::*,
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, WithScrollbar,
prelude::*,
};
pub struct AcpThreadHistory {
@@ -26,8 +26,6 @@ pub struct AcpThreadHistory {
visible_items: Vec<ListItemType>,
scrollbar_visibility: bool,
scrollbar_state: ScrollbarState,
local_timezone: UtcOffset,
_update_task: Task<()>,
@@ -70,7 +68,7 @@ impl AcpThreadHistory {
) -> Self {
let search_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_placeholder_text("Search threads...", cx);
editor.set_placeholder_text("Search threads...", window, cx);
editor
});
@@ -90,7 +88,6 @@ impl AcpThreadHistory {
});
let scroll_handle = UniformListScrollHandle::default();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
history_store,
@@ -99,8 +96,6 @@ impl AcpThreadHistory {
hovered_index: None,
visible_items: Default::default(),
search_editor,
scrollbar_visibility: true,
scrollbar_state,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
)
@@ -339,43 +334,6 @@ impl AcpThreadHistory {
task.detach_and_log_err(cx);
}
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
return None;
}
Some(
div()
.occlude()
.id("thread-history-scroll")
.h_full()
.bg(cx.theme().colors().panel_background.opacity(0.8))
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.absolute()
.right_1()
.top_0()
.bottom_0()
.w_4()
.pl_1()
.cursor_default()
.on_mouse_move(cx.listener(|_, _, _window, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _window, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _window, cx| {
cx.stop_propagation();
})
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
}
fn render_list_items(
&mut self,
range: Range<usize>,
@@ -491,7 +449,7 @@ impl Focusable for AcpThreadHistory {
}
impl Render for AcpThreadHistory {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("ThreadHistory")
.size_full()
@@ -542,22 +500,24 @@ impl Render for AcpThreadHistory {
),
)
} else {
view.pr_5()
.child(
uniform_list(
"thread-history",
self.visible_items.len(),
cx.processor(|this, range: Range<usize>, window, cx| {
this.render_list_items(range, window, cx)
}),
)
.p_1()
.track_scroll(self.scroll_handle.clone())
.flex_grow(),
view.child(
uniform_list(
"thread-history",
self.visible_items.len(),
cx.processor(|this, range: Range<usize>, window, cx| {
this.render_list_items(range, window, cx)
}),
)
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
div.child(scrollbar)
})
.p_1()
.pr_4()
.track_scroll(self.scroll_handle.clone())
.flex_grow(),
)
.vertical_scrollbar_for(
self.scroll_handle.clone(),
window,
cx,
)
}
})
}

View File

@@ -7,13 +7,14 @@ use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog;
use agent_client_protocol::{self as acp, PromptCapabilities};
use agent_servers::{AgentServer, AgentServerDelegate};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
use anyhow::{Context as _, Result, anyhow, bail};
use anyhow::{Result, anyhow, bail};
use arrayvec::ArrayVec;
use audio::{Audio, Sound};
use buffer_diff::BufferDiff;
use client::zed_urls;
use cloud_llm_client::PlanV1;
use collections::{HashMap, HashSet};
use editor::scroll::Autoscroll;
use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects};
@@ -23,10 +24,9 @@ use futures::FutureExt as _;
use gpui::{
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window,
WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*,
pulsating_between,
ListOffset, ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task,
TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, WindowHandle, div,
ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*, pulsating_between,
};
use language::Buffer;
@@ -35,8 +35,8 @@ use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use project::{Project, ProjectEntryId};
use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::{Settings as _, SettingsStore};
use std::cell::{Cell, RefCell};
use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
use std::cell::RefCell;
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
@@ -46,7 +46,7 @@ use text::Anchor;
use theme::{AgentFontSize, ThemeSettings};
use ui::{
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
@@ -61,9 +61,9 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
use crate::agent_diff::AgentDiff;
use crate::profile_selector::{ProfileProvider, ProfileSelector};
use crate::ui::preview::UsageCallout;
use crate::ui::{
AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip,
UsageCallout,
};
use crate::{
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
@@ -71,9 +71,6 @@ use crate::{
RejectOnce, ToggleBurnMode, ToggleProfileSelector,
};
pub const MIN_EDITOR_LINES: usize = 4;
pub const MAX_EDITOR_LINES: usize = 8;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum ThreadFeedback {
Positive,
@@ -250,6 +247,7 @@ impl ThreadFeedbackState {
);
editor.set_placeholder_text(
"What went wrong? Share your feedback so we can improve.",
window,
cx,
);
editor
@@ -279,7 +277,6 @@ pub struct AcpThreadView {
thread_error: Option<ThreadError>,
thread_feedback: ThreadFeedbackState,
list_state: ListState,
scrollbar_state: ScrollbarState,
auth_task: Option<Task<()>>,
expanded_tool_calls: HashSet<acp::ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
@@ -288,7 +285,7 @@ pub struct AcpThreadView {
editor_expanded: bool,
should_be_following: bool,
editing_message: Option<usize>,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
is_loading_contents: bool,
new_server_version_available: Option<SharedString>,
@@ -332,7 +329,7 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let available_commands = Rc::new(RefCell::new(vec![]));
let placeholder = if agent.name() == "Zed Agent" {
@@ -355,10 +352,10 @@ impl AcpThreadView {
prompt_capabilities.clone(),
available_commands.clone(),
agent.name(),
placeholder,
&placeholder,
editor::EditorMode::AutoHeight {
min_lines: MIN_EDITOR_LINES,
max_lines: Some(MAX_EDITOR_LINES),
min_lines: AgentSettings::get_global(cx).message_editor_min_lines,
max_lines: Some(AgentSettings::get_global(cx).set_message_editor_max_lines()),
},
window,
cx,
@@ -403,8 +400,7 @@ impl AcpThreadView {
notifications: Vec::new(),
notification_subscriptions: HashMap::default(),
list_state: list_state.clone(),
scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
list_state: list_state,
thread_retry_status: None,
thread_error: None,
thread_feedback: Default::default(),
@@ -557,7 +553,7 @@ impl AcpThreadView {
let action_log = thread.read(cx).action_log().clone();
this.prompt_capabilities
.set(thread.read(cx).prompt_capabilities());
.replace(thread.read(cx).prompt_capabilities());
let count = thread.read(cx).entries().len();
this.entry_view_state.update(cx, |view_state, cx| {
@@ -858,10 +854,11 @@ impl AcpThreadView {
cx,
)
} else {
let agent_settings = AgentSettings::get_global(cx);
editor.set_mode(
EditorMode::AutoHeight {
min_lines: MIN_EDITOR_LINES,
max_lines: Some(MAX_EDITOR_LINES),
min_lines: agent_settings.message_editor_min_lines,
max_lines: Some(agent_settings.set_message_editor_max_lines()),
},
cx,
)
@@ -1371,7 +1368,7 @@ impl AcpThreadView {
}
AcpThreadEvent::PromptCapabilitiesUpdated => {
self.prompt_capabilities
.set(thread.read(cx).prompt_capabilities());
.replace(thread.read(cx).prompt_capabilities());
}
AcpThreadEvent::TokenUsageUpdated => {}
AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
@@ -1388,11 +1385,13 @@ impl AcpThreadView {
name: "login".to_owned(),
description: "Authenticate".to_owned(),
input: None,
meta: None,
});
available_commands.push(acp::AvailableCommand {
name: "logout".to_owned(),
description: "Authenticate".to_owned(),
input: None,
meta: None,
});
}
@@ -1583,19 +1582,6 @@ impl AcpThreadView {
window.spawn(cx, async move |cx| {
let mut task = login.clone();
task.command = task
.command
.map(|command| anyhow::Ok(shlex::try_quote(&command)?.to_string()))
.transpose()?;
task.args = task
.args
.iter()
.map(|arg| {
Ok(shlex::try_quote(arg)
.context("Failed to quote argument")?
.to_string())
})
.collect::<Result<Vec<_>>>()?;
task.full_label = task.label.clone();
task.id = task::TaskId(format!("external-agent-{}-login", task.label));
task.command_label = task.label.clone();
@@ -3196,10 +3182,14 @@ impl AcpThreadView {
};
Button::new(SharedString::from(method_id.clone()), name)
.when(ix == 0, |el| {
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
})
.label_size(LabelSize::Small)
.map(|this| {
if ix == 0 {
this.style(ButtonStyle::Tinted(TintColor::Warning))
} else {
this.style(ButtonStyle::Outlined)
}
})
.on_click({
cx.listener(move |this, _, window, cx| {
telemetry::event!(
@@ -4760,39 +4750,6 @@ impl AcpThreadView {
cx.notify();
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.id("acp-thread-scrollbar")
.occlude()
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
}
fn render_token_limit_callout(
&self,
line_height: Pixels,
@@ -4874,7 +4831,9 @@ impl AcpThreadView {
return None;
}
let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree);
let plan = user_store
.plan()
.unwrap_or(cloud_llm_client::Plan::V1(PlanV1::ZedFree));
let usage = user_store.model_request_usage()?;
@@ -5133,13 +5092,12 @@ impl AcpThreadView {
cx: &mut Context<Self>,
) -> Callout {
let error_message = match plan {
cloud_llm_client::Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
cloud_llm_client::Plan::ZedProTrial | cloud_llm_client::Plan::ZedFree => {
"Upgrade to Zed Pro for more prompts."
cloud_llm_client::Plan::V1(PlanV1::ZedPro) => {
"Upgrade to usage-based billing for more prompts."
}
cloud_llm_client::Plan::ZedProV2
| cloud_llm_client::Plan::ZedProTrialV2
| cloud_llm_client::Plan::ZedFreeV2 => "",
cloud_llm_client::Plan::V1(PlanV1::ZedProTrial)
| cloud_llm_client::Plan::V1(PlanV1::ZedFree) => "Upgrade to Zed Pro for more prompts.",
cloud_llm_client::Plan::V2(_) => "",
};
Callout::new()
@@ -5365,23 +5323,27 @@ impl Render for AcpThreadView {
configuration_view,
pending_auth_method,
..
} => self.render_auth_required_state(
connection,
description.as_ref(),
configuration_view.as_ref(),
pending_auth_method.as_ref(),
window,
cx,
),
} => self
.render_auth_required_state(
connection,
description.as_ref(),
configuration_view.as_ref(),
pending_auth_method.as_ref(),
window,
cx,
)
.into_any(),
ThreadState::Loading { .. } => v_flex()
.flex_1()
.child(self.render_recent_history(window, cx)),
.child(self.render_recent_history(window, cx))
.into_any(),
ThreadState::LoadError(e) => v_flex()
.flex_1()
.size_full()
.items_center()
.justify_end()
.child(self.render_load_error(e, window, cx)),
.child(self.render_load_error(e, window, cx))
.into_any(),
ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
if has_messages {
this.child(
@@ -5401,9 +5363,11 @@ impl Render for AcpThreadView {
.flex_grow()
.into_any(),
)
.child(self.render_vertical_scrollbar(cx))
.vertical_scrollbar_for(self.list_state.clone(), window, cx)
.into_any()
} else {
this.child(self.render_recent_history(window, cx))
.into_any()
}
}),
})
@@ -5705,6 +5669,23 @@ pub(crate) mod tests {
});
}
#[gpui::test]
async fn test_spawn_external_agent_login_handles_spaces(cx: &mut TestAppContext) {
init_test(cx);
// Verify paths with spaces aren't pre-quoted
let path_with_spaces = "/Users/test/Library/Application Support/Zed/cli.js";
let login_task = task::SpawnInTerminal {
command: Some("node".to_string()),
args: vec![path_with_spaces.to_string(), "/login".to_string()],
..Default::default()
};
// Args should be passed as-is, not pre-quoted
assert!(!login_task.args[0].starts_with('"'));
assert!(!login_task.args[0].starts_with('\''));
}
#[gpui::test]
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
init_test(cx);
@@ -5719,6 +5700,7 @@ pub(crate) mod tests {
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
};
let connection =
StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
@@ -5727,6 +5709,7 @@ pub(crate) mod tests {
id: acp::PermissionOptionId("1".into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
meta: None,
}],
)]));
@@ -5903,6 +5886,7 @@ pub(crate) mod tests {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
@@ -5962,6 +5946,7 @@ pub(crate) mod tests {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
@@ -5988,6 +5973,7 @@ pub(crate) mod tests {
) -> Task<gpui::Result<acp::PromptResponse>> {
Task::ready(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
}))
}
@@ -6071,11 +6057,13 @@ pub(crate) mod tests {
path: "/project/test1.txt".into(),
old_text: Some("old content 1".into()),
new_text: "new content 1".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
})]);
thread
@@ -6112,11 +6100,13 @@ pub(crate) mod tests {
path: "/project/test2.txt".into(),
old_text: Some("old content 2".into()),
new_text: "new content 2".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
})]);
thread
@@ -6194,6 +6184,7 @@ pub(crate) mod tests {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
}]);
@@ -6283,6 +6274,7 @@ pub(crate) mod tests {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
}]);
@@ -6326,6 +6318,7 @@ pub(crate) mod tests {
content: acp::ContentBlock::Text(acp::TextContent {
text: "New Response".into(),
annotations: None,
meta: None,
}),
}]);
@@ -6418,6 +6411,7 @@ pub(crate) mod tests {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
},
cx,

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
mod add_llm_provider_modal;
mod configure_context_server_modal;
mod configure_context_server_tools_modal;
mod manage_profiles_modal;
mod tool_picker;
@@ -8,7 +9,7 @@ use std::{ops::Range, sync::Arc};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan;
use cloud_llm_client::{Plan, PlanV1, PlanV2};
use collections::HashMap;
use context_server::ContextServerId;
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
@@ -25,30 +26,25 @@ use language_model::{
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
agent_server_store::{
AgentServerCommand, AgentServerStore, AllAgentServersSettings, CLAUDE_CODE_NAME,
CustomAgentServerSettings, GEMINI_NAME,
},
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, GEMINI_NAME},
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip,
prelude::*,
Indicator, PopoverMenu, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::{
AddContextServer, ExternalAgent, NewExternalAgentThread,
AddContextServer,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
placeholder_command,
};
pub struct AgentConfiguration {
@@ -64,7 +60,6 @@ pub struct AgentConfiguration {
tools: Entity<ToolWorkingSet>,
_registry_subscription: Subscription,
scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
_check_for_gemini: Task<()>,
}
@@ -101,9 +96,6 @@ impl AgentConfiguration {
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
.detach();
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
fs,
language_registry,
@@ -116,8 +108,7 @@ impl AgentConfiguration {
expanded_provider_configurations: HashMap::default(),
tools,
_registry_subscription: registry_subscription,
scroll_handle,
scrollbar_state,
scroll_handle: ScrollHandle::new(),
_check_for_gemini: Task::ready(()),
};
this.build_provider_configuration_views(window, cx);
@@ -206,9 +197,8 @@ impl AgentConfiguration {
.when(is_expanded, |this| this.mb_2())
.child(
div()
.opacity(0.6)
.px_2()
.child(Divider::horizontal().color(DividerColor::Border)),
.child(Divider::horizontal().color(DividerColor::BorderFaded)),
)
.child(
h_flex()
@@ -233,7 +223,7 @@ impl AgentConfiguration {
.child(
h_flex()
.w_full()
.gap_2()
.gap_1p5()
.child(
Icon::new(provider.icon())
.size(IconSize::Small)
@@ -280,13 +270,28 @@ impl AgentConfiguration {
*is_expanded = !*is_expanded;
}
})),
)
.when(provider.is_authenticated(cx), |parent| {
),
)
.child(
v_flex()
.w_full()
.px_2()
.gap_1()
.when(is_expanded, |parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
None => parent.child(Label::new(format!(
"No configuration view for {provider_name}",
))),
})
.when(is_expanded && provider.is_authenticated(cx), |parent| {
parent.child(
Button::new(
SharedString::from(format!("new-thread-{provider_id}")),
"Start New Thread",
)
.full_width()
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Thread)
.icon_size(IconSize::Small)
@@ -303,17 +308,6 @@ impl AgentConfiguration {
)
}),
)
.child(
div()
.w_full()
.px_2()
.when(is_expanded, |parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
None => parent.child(Label::new(format!(
"No configuration view for {provider_name}",
))),
}),
)
}
fn render_provider_configuration_section(
@@ -347,6 +341,8 @@ impl AgentConfiguration {
PopoverMenu::new("add-provider-popover")
.trigger(
Button::new("add-provider", "Add Provider")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
@@ -419,8 +415,8 @@ impl AgentConfiguration {
always_allow_tool_actions,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
settings.set_always_allow_tool_actions(allow);
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_always_allow_tool_actions(allow);
});
},
)
@@ -437,8 +433,11 @@ impl AgentConfiguration {
single_file_review,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
settings.set_single_file_review(allow);
update_settings_file(fs.clone(), cx, move |settings, _| {
settings
.agent
.get_or_insert_default()
.set_single_file_review(allow);
});
},
)
@@ -457,8 +456,8 @@ impl AgentConfiguration {
play_sound_when_agent_done,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
settings.set_play_sound_when_agent_done(allow);
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_play_sound_when_agent_done(allow);
});
},
)
@@ -477,8 +476,8 @@ impl AgentConfiguration {
use_modifier_to_send,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
settings.set_use_modifier_to_send(allow);
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_use_modifier_to_send(allow);
});
},
)
@@ -515,11 +514,15 @@ impl AgentConfiguration {
.blend(cx.theme().colors().text_accent.opacity(0.2));
let (plan_name, label_color, bg_color) = match plan {
Plan::ZedFree | Plan::ZedFreeV2 => ("Free", Color::Default, free_chip_bg),
Plan::ZedProTrial | Plan::ZedProTrialV2 => {
Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree) => {
("Free", Color::Default, free_chip_bg)
}
Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial) => {
("Pro Trial", Color::Accent, pro_chip_bg)
}
Plan::ZedPro | Plan::ZedProV2 => ("Pro", Color::Accent, pro_chip_bg),
Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro) => {
("Pro", Color::Accent, pro_chip_bg)
}
};
Chip::new(plan_name.to_string())
@@ -531,10 +534,6 @@ impl AgentConfiguration {
}
}
fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
cx.theme().colors().background.opacity(0.25)
}
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
cx.theme().colors().border.opacity(0.6)
}
@@ -544,61 +543,58 @@ impl AgentConfiguration {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let context_server_ids = self.context_server_store.read(cx).configured_server_ids();
let mut registry_descriptors = self
.context_server_store
.read(cx)
.all_registry_descriptor_ids(cx);
let server_count = registry_descriptors.len();
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.gap_0p5()
.child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(
Label::new(
"All context servers connected through the Model Context Protocol.",
)
.color(Color::Muted),
),
// Sort context servers: non-mcp-server ones first, then mcp-server ones
registry_descriptors.sort_by(|a, b| {
let has_mcp_prefix_a = a.0.starts_with("mcp-server-");
let has_mcp_prefix_b = b.0.starts_with("mcp-server-");
match (has_mcp_prefix_a, has_mcp_prefix_b) {
// If one has mcp-server- prefix and other doesn't, non-mcp comes first
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
// If both have same prefix status, sort by appropriate key
_ => {
let get_sort_key = |server_id: &str| -> String {
if let Some(suffix) = server_id.strip_prefix("mcp-server-") {
suffix.to_string()
} else {
server_id.to_string()
}
};
let key_a = get_sort_key(&a.0);
let key_b = get_sort_key(&b.0);
key_a.cmp(&key_b)
}
}
});
let add_server_popover = PopoverMenu::new("add-server-popover")
.trigger(
Button::new("add-server", "Add Server")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.children(
context_server_ids.into_iter().map(|context_server_id| {
self.render_context_server(context_server_id, window, cx)
}),
)
.child(
h_flex()
.justify_between()
.gap_1p5()
.child(
h_flex().w_full().child(
Button::new("add-context-server", "Add Custom Server")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.full_width()
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.on_click(|_event, window, cx| {
window.dispatch_action(AddContextServer.boxed_clone(), cx)
}),
),
)
.child(
h_flex().w_full().child(
Button::new(
"install-context-server-extensions",
"Install MCP Extensions",
)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.full_width()
.icon(IconName::ToolHammer)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.on_click(|_event, window, cx| {
.anchor(gpui::Corner::TopRight)
.menu({
move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.entry("Add Custom Server", None, {
|window, cx| window.dispatch_action(AddContextServer.boxed_clone(), cx)
})
.entry("Install from Extensions", None, {
|window, cx| {
window.dispatch_action(
zed_actions::Extensions {
category_filter: Some(
@@ -609,10 +605,76 @@ impl AgentConfiguration {
.boxed_clone(),
cx,
)
}),
),
),
}
})
}))
}
});
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.w_full()
.items_start()
.justify_between()
.gap_1()
.child(
v_flex()
.gap_0p5()
.child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(
Label::new(
"All MCP servers connected directly or via a Zed extension.",
)
.color(Color::Muted),
),
)
.child(add_server_popover),
)
.child(v_flex().w_full().gap_1().map(|parent| {
if registry_descriptors.is_empty() {
parent.child(
h_flex()
.p_4()
.justify_center()
.border_1()
.border_dashed()
.border_color(cx.theme().colors().border.opacity(0.6))
.rounded_sm()
.child(
Label::new("No MCP servers added yet.")
.color(Color::Muted)
.size(LabelSize::Small),
),
)
} else {
{
parent.children(registry_descriptors.into_iter().enumerate().flat_map(
|(index, context_server_id)| {
let mut elements: Vec<AnyElement> = vec![
self.render_context_server(context_server_id, window, cx)
.into_any_element(),
];
if index < server_count - 1 {
elements.push(
Divider::horizontal()
.color(DividerColor::BorderFaded)
.into_any_element(),
);
}
elements
},
))
}
}
}))
}
fn render_context_server(
@@ -705,7 +767,7 @@ impl AgentConfiguration {
IconButton::new("context-server-config-menu", IconName::Settings)
.icon_color(Color::Muted)
.icon_size(IconSize::Small),
Tooltip::text("Open MCP server options"),
Tooltip::text("Configure MCP Server"),
)
.anchor(Corner::TopRight)
.menu({
@@ -714,6 +776,8 @@ impl AgentConfiguration {
let language_registry = self.language_registry.clone();
let context_server_store = self.context_server_store.clone();
let workspace = self.workspace.clone();
let tools = self.tools.clone();
move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.entry("Configure Server", None, {
@@ -730,7 +794,28 @@ impl AgentConfiguration {
)
.detach_and_log_err(cx);
}
})
}).when(tool_count >= 1, |this| this.entry("View Tools", None, {
let context_server_id = context_server_id.clone();
let tools = tools.clone();
let workspace = workspace.clone();
move |window, cx| {
let context_server_id = context_server_id.clone();
let tools = tools.clone();
let workspace = workspace.clone();
workspace.update(cx, |workspace, cx| {
ConfigureContextServerToolsModal::toggle(
context_server_id,
tools,
workspace,
window,
cx,
);
})
.ok();
}
}))
.separator()
.entry("Uninstall", None, {
let fs = fs.clone();
@@ -776,14 +861,14 @@ impl AgentConfiguration {
async move |cx| {
uninstall_extension_task.await?;
cx.update(|cx| {
update_settings_file::<ProjectSettings>(
update_settings_file(
fs.clone(),
cx,
{
let context_server_id =
context_server_id.clone();
move |settings, _| {
settings
settings.project
.context_servers
.remove(&context_server_id.0);
}
@@ -801,17 +886,11 @@ impl AgentConfiguration {
v_flex()
.id(item_id.clone())
.border_1()
.rounded_md()
.border_color(self.card_item_border_color(cx))
.bg(self.card_item_bg_color(cx))
.overflow_hidden()
.child(
h_flex()
.p_1()
.justify_between()
.when(
error.is_some() || are_tools_expanded && tool_count >= 1,
error.is_none() && are_tools_expanded && tool_count >= 1,
|element| {
element
.border_b_1()
@@ -820,40 +899,25 @@ impl AgentConfiguration {
)
.child(
h_flex()
.child(
Disclosure::new(
"tool-list-disclosure",
are_tools_expanded || error.is_some(),
)
.disabled(tool_count == 0)
.on_click(cx.listener({
let context_server_id = context_server_id.clone();
move |this, _event, _window, _cx| {
let is_open = this
.expanded_context_server_tools
.entry(context_server_id.clone())
.or_insert(false);
*is_open = !*is_open;
}
})),
)
.flex_1()
.min_w_0()
.child(
h_flex()
.id(SharedString::from(format!("tooltip-{}", item_id)))
.h_full()
.w_3()
.mx_1()
.mr_2()
.justify_center()
.tooltip(Tooltip::text(tooltip_text))
.child(status_indicator),
)
.child(Label::new(item_id).ml_0p5())
.child(Label::new(item_id).truncate())
.child(
div()
.id("extension-source")
.mt_0p5()
.mx_1()
.flex_none()
.tooltip(Tooltip::text(source_tooltip))
.child(
Icon::new(source_icon)
@@ -875,78 +939,65 @@ impl AgentConfiguration {
)
.child(
h_flex()
.gap_1()
.gap_0p5()
.flex_none()
.child(context_server_configuration_menu)
.child(
Switch::new("context-server-switch", is_running.into())
.color(SwitchColor::Accent)
.on_click({
let context_server_manager =
self.context_server_store.clone();
let fs = self.fs.clone();
Switch::new("context-server-switch", is_running.into())
.color(SwitchColor::Accent)
.on_click({
let context_server_manager = self.context_server_store.clone();
let fs = self.fs.clone();
move |state, _window, cx| {
let is_enabled = match state {
ToggleState::Unselected
| ToggleState::Indeterminate => {
context_server_manager.update(
cx,
|this, cx| {
this.stop_server(
&context_server_id,
cx,
)
.log_err();
},
);
false
}
ToggleState::Selected => {
context_server_manager.update(
cx,
|this, cx| {
if let Some(server) =
this.get_server(&context_server_id)
{
this.start_server(server, cx);
}
},
);
true
}
};
update_settings_file::<ProjectSettings>(
fs.clone(),
cx,
{
let context_server_id =
context_server_id.clone();
move |settings, _| {
settings
.context_servers
.entry(context_server_id.0)
.or_insert_with(|| {
ContextServerSettings::Extension {
enabled: is_enabled,
settings: serde_json::json!({}),
}
})
.set_enabled(is_enabled);
move |state, _window, cx| {
let is_enabled = match state {
ToggleState::Unselected
| ToggleState::Indeterminate => {
context_server_manager.update(cx, |this, cx| {
this.stop_server(&context_server_id, cx)
.log_err();
});
false
}
ToggleState::Selected => {
context_server_manager.update(cx, |this, cx| {
if let Some(server) =
this.get_server(&context_server_id)
{
this.start_server(server, cx);
}
},
);
}
}),
),
});
true
}
};
update_settings_file(fs.clone(), cx, {
let context_server_id = context_server_id.clone();
move |settings, _| {
settings
.project
.context_servers
.entry(context_server_id.0)
.or_insert_with(|| {
settings::ContextServerSettingsContent::Extension {
enabled: is_enabled,
settings: serde_json::json!({}),
}
})
.set_enabled(is_enabled);
}
});
}
}),
),
),
)
.map(|parent| {
if let Some(error) = error {
return parent.child(
h_flex()
.p_2()
.gap_2()
.pr_4()
.items_start()
.child(
h_flex()
@@ -974,37 +1025,11 @@ impl AgentConfiguration {
return parent;
}
parent.child(v_flex().py_1p5().px_1().gap_1().children(
tools.iter().enumerate().map(|(ix, tool)| {
h_flex()
.id(("tool-item", ix))
.px_1()
.gap_2()
.justify_between()
.hover(|style| style.bg(cx.theme().colors().element_hover))
.rounded_sm()
.child(
Label::new(tool.name())
.buffer_font(cx)
.size(LabelSize::Small),
)
.child(
Icon::new(IconName::Info)
.size(IconSize::Small)
.color(Color::Ignored),
)
.tooltip(Tooltip::text(tool.description()))
}),
))
parent
})
}
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let custom_settings = cx
.global::<SettingsStore>()
.get::<AllAgentServersSettings>(None)
.custom
.clone();
let user_defined_agents = self
.agent_server_store
.read(cx)
@@ -1012,22 +1037,12 @@ impl AgentConfiguration {
.filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME)
.cloned()
.collect::<Vec<_>>();
let user_defined_agents = user_defined_agents
.into_iter()
.map(|name| {
self.render_agent_server(
IconName::Ai,
name.clone(),
ExternalAgent::Custom {
name: name.clone().into(),
command: custom_settings
.get(&name.0)
.map(|settings| settings.command.clone())
.unwrap_or(placeholder_command()),
},
cx,
)
.into_any_element()
self.render_agent_server(IconName::Ai, name)
.into_any_element()
})
.collect::<Vec<_>>();
@@ -1051,6 +1066,8 @@ impl AgentConfiguration {
.child(Headline::new("External Agents"))
.child(
Button::new("add-agent", "Add Agent")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
@@ -1083,14 +1100,11 @@ impl AgentConfiguration {
.child(self.render_agent_server(
IconName::AiGemini,
"Gemini CLI",
ExternalAgent::Gemini,
cx,
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiClaude,
"Claude Code",
ExternalAgent::ClaudeCode,
cx,
))
.children(user_defined_agents),
)
@@ -1100,46 +1114,18 @@ impl AgentConfiguration {
&self,
icon: IconName,
name: impl Into<SharedString>,
agent: ExternalAgent,
cx: &mut Context<Self>,
) -> impl IntoElement {
let name = name.into();
h_flex()
.p_1()
.pl_2()
.gap_1p5()
.justify_between()
.border_1()
.rounded_md()
.border_color(self.card_item_border_color(cx))
.bg(self.card_item_bg_color(cx))
.overflow_hidden()
.child(
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(name.clone())),
)
.child(
Button::new(
SharedString::from(format!("start_acp_thread-{name}")),
"Start New Thread",
)
.label_size(LabelSize::Small)
.icon(IconName::Thread)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(agent.clone()),
}
.boxed_clone(),
cx,
);
}),
)
h_flex().gap_1p5().justify_between().child(
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(name.into()))
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
),
)
}
}
@@ -1153,42 +1139,21 @@ impl Render for AgentConfiguration {
.size_full()
.pb_8()
.bg(cx.theme().colors().panel_background)
.child(
v_flex()
.id("assistant-configuration-content")
.track_scroll(&self.scroll_handle)
.size_full()
.overflow_y_scroll()
.child(self.render_general_settings_section(cx))
.child(self.render_agent_servers_section(cx))
.child(self.render_context_servers_section(window, cx))
.child(self.render_provider_configuration_section(cx)),
)
.child(
div()
.id("assistant-configuration-scrollbar")
.occlude()
.absolute()
.right(px(3.))
.top_0()
.bottom_0()
.pb_6()
.w(px(12.))
.cursor_default()
.on_mouse_move(cx.listener(|_, _, _window, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _window, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _window, cx| {
cx.stop_propagation();
})
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
.size_full()
.child(
v_flex()
.id("assistant-configuration-content")
.track_scroll(&self.scroll_handle)
.size_full()
.overflow_y_scroll()
.child(self.render_general_settings_section(cx))
.child(self.render_agent_servers_section(cx))
.child(self.render_context_servers_section(window, cx))
.child(self.render_provider_configuration_section(cx)),
)
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
)
}
}
@@ -1256,15 +1221,12 @@ fn show_unable_to_uninstall_extension_with_context_server(
let context_server_id = context_server_id.clone();
async move |_workspace_handle, cx| {
cx.update(|cx| {
update_settings_file::<ProjectSettings>(
fs,
cx,
move |settings, _| {
settings
.context_servers
.remove(&context_server_id.0);
},
);
update_settings_file(fs, cx, move |settings, _| {
settings
.project
.context_servers
.remove(&context_server_id.0);
});
})?;
anyhow::Ok(())
}
@@ -1302,7 +1264,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
let settings = cx.global::<SettingsStore>();
let mut unique_server_name = None;
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
let edits = settings.edits_for_update(&text, |settings| {
let server_name: Option<SharedString> = (0..u8::MAX)
.map(|i| {
if i == 0 {
@@ -1311,20 +1273,27 @@ async fn open_new_agent_servers_entry_in_settings_editor(
format!("your_agent_{}", i).into()
}
})
.find(|name| !file.custom.contains_key(name));
.find(|name| {
!settings
.agent_servers
.as_ref()
.is_some_and(|agent_servers| agent_servers.custom.contains_key(name))
});
if let Some(server_name) = server_name {
unique_server_name = Some(server_name.clone());
file.custom.insert(
server_name,
CustomAgentServerSettings {
command: AgentServerCommand {
settings
.agent_servers
.get_or_insert_default()
.custom
.insert(
server_name,
settings::CustomAgentServerSettings {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
default_mode: None,
},
default_mode: None,
},
);
);
}
});

View File

@@ -5,11 +5,8 @@ use collections::HashSet;
use fs::Fs;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
use language_model::LanguageModelRegistry;
use language_models::{
AllLanguageModelSettings, OpenAiCompatibleSettingsContent,
provider::open_ai_compatible::{AvailableModel, ModelCapabilities},
};
use settings::update_settings_file;
use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities};
use settings::{OpenAiCompatibleSettingsContent, update_settings_file};
use ui::{
Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
};
@@ -238,14 +235,19 @@ fn save_provider_to_settings(
task.await
.map_err(|_| "Failed to write API key to keychain")?;
cx.update(|cx| {
update_settings_file::<AllLanguageModelSettings>(fs, cx, |settings, _cx| {
settings.openai_compatible.get_or_insert_default().insert(
provider_name,
OpenAiCompatibleSettingsContent {
api_url,
available_models: models,
},
);
update_settings_file(fs, cx, |settings, _cx| {
settings
.language_models
.get_or_insert_default()
.openai_compatible
.get_or_insert_default()
.insert(
provider_name,
OpenAiCompatibleSettingsContent {
api_url,
available_models: models,
},
);
});
})
.ok();

View File

@@ -422,18 +422,17 @@ impl ConfigureContextServerModal {
workspace.update(cx, |workspace, cx| {
let fs = workspace.app_state().fs.clone();
let original_server_id = self.original_server_id.clone();
update_settings_file::<ProjectSettings>(
fs.clone(),
cx,
move |project_settings, _| {
if let Some(original_id) = original_server_id {
if original_id != id {
project_settings.context_servers.remove(&original_id.0);
}
update_settings_file(fs.clone(), cx, move |current, _| {
if let Some(original_id) = original_server_id {
if original_id != id {
current.project.context_servers.remove(&original_id.0);
}
project_settings.context_servers.insert(id.0, settings);
},
);
}
current
.project
.context_servers
.insert(id.0, settings.into());
});
});
} else if let Some(existing_server) = existing_server {
self.context_server_store

View File

@@ -0,0 +1,176 @@
use assistant_tool::{ToolSource, ToolWorkingSet};
use context_server::ContextServerId;
use gpui::{
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Window, prelude::*,
};
use ui::{Divider, DividerColor, Modal, ModalHeader, WithScrollbar, prelude::*};
use workspace::{ModalView, Workspace};
pub struct ConfigureContextServerToolsModal {
context_server_id: ContextServerId,
tools: Entity<ToolWorkingSet>,
focus_handle: FocusHandle,
expanded_tools: std::collections::HashMap<String, bool>,
scroll_handle: ScrollHandle,
}
impl ConfigureContextServerToolsModal {
fn new(
context_server_id: ContextServerId,
tools: Entity<ToolWorkingSet>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
Self {
context_server_id,
tools,
focus_handle: cx.focus_handle(),
expanded_tools: std::collections::HashMap::new(),
scroll_handle: ScrollHandle::new(),
}
}
pub fn toggle(
context_server_id: ContextServerId,
tools: Entity<ToolWorkingSet>,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
workspace.toggle_modal(window, cx, |window, cx| {
Self::new(context_server_id, tools, window, cx)
});
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent)
}
fn render_modal_content(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let tools_by_source = self.tools.read(cx).tools_by_source(cx);
let server_tools = tools_by_source
.get(&ToolSource::ContextServer {
id: self.context_server_id.0.clone().into(),
})
.map(|tools| tools.as_slice())
.unwrap_or(&[]);
div()
.size_full()
.pb_2()
.child(
v_flex()
.id("modal_content")
.px_2()
.gap_1()
.max_h_128()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.children(server_tools.iter().enumerate().flat_map(|(index, tool)| {
let tool_name = tool.name();
let is_expanded = self
.expanded_tools
.get(&tool_name)
.copied()
.unwrap_or(false);
let icon = if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
};
let mut items = vec![
v_flex()
.child(
h_flex()
.id(SharedString::from(format!("tool-header-{}", index)))
.py_1()
.pl_1()
.pr_2()
.w_full()
.justify_between()
.rounded_sm()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.child(
Label::new(tool_name.clone())
.buffer_font(cx)
.size(LabelSize::Small),
)
.child(
Icon::new(icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.on_click(cx.listener({
move |this, _event, _window, _cx| {
let current = this
.expanded_tools
.get(&tool_name)
.copied()
.unwrap_or(false);
this.expanded_tools
.insert(tool_name.clone(), !current);
_cx.notify();
}
})),
)
.when(is_expanded, |this| {
this.child(
Label::new(tool.description()).color(Color::Muted).mx_1(),
)
})
.into_any_element(),
];
if index < server_tools.len() - 1 {
items.push(
h_flex()
.w_full()
.child(Divider::horizontal().color(DividerColor::BorderVariant))
.into_any_element(),
);
}
items
})),
)
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
.into_any_element()
}
}
impl ModalView for ConfigureContextServerToolsModal {}
impl Focusable for ConfigureContextServerToolsModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for ConfigureContextServerToolsModal {}
impl Render for ConfigureContextServerToolsModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.key_context("ContextServerToolsModal")
.occlude()
.elevation_3(cx)
.w(rems(34.))
.on_action(cx.listener(Self::cancel))
.track_focus(&self.focus_handle)
.child(
Modal::new("configure-context-server-tools", None::<ScrollHandle>)
.header(
ModalHeader::new()
.headline(format!("Tools from {}", self.context_server_id.0))
.show_dismiss_button(true),
)
.child(self.render_modal_content(window, cx)),
)
}
}

View File

@@ -2,7 +2,7 @@ mod profile_modal_header;
use std::sync::Arc;
use agent_settings::{AgentProfileId, AgentSettings, builtin_profiles};
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
use assistant_tool::ToolWorkingSet;
use editor::Editor;
use fs::Fs;
@@ -16,7 +16,6 @@ use workspace::{ModalView, Workspace};
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AgentPanel, ManageProfiles};
use agent::agent_profile::AgentProfile;
use super::tool_picker::ToolPickerMode;
@@ -156,7 +155,7 @@ impl ManageProfilesModal {
) {
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
name_editor.update(cx, |editor, cx| {
editor.set_placeholder_text("Profile name", cx);
editor.set_placeholder_text("Profile name", window, cx);
});
self.mode = Mode::NewProfile(NewProfileMode {

View File

@@ -1,14 +1,11 @@
use std::{collections::BTreeMap, sync::Arc};
use agent_settings::{
AgentProfileContent, AgentProfileId, AgentProfileSettings, AgentSettings, AgentSettingsContent,
ContextServerPresetContent,
};
use agent_settings::{AgentProfileId, AgentProfileSettings};
use assistant_tool::{ToolSource, ToolWorkingSet};
use fs::Fs;
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
use picker::{Picker, PickerDelegate};
use settings::update_settings_file;
use settings::{AgentProfileContent, ContextServerPresetContent, update_settings_file};
use ui::{ListItem, ListItemSpacing, prelude::*};
use util::ResultExt as _;
@@ -266,15 +263,19 @@ impl PickerDelegate for ToolPickerDelegate {
is_enabled
};
update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
update_settings_file(self.fs.clone(), cx, {
let profile_id = self.profile_id.clone();
let default_profile = self.profile_settings.clone();
let server_id = server_id.clone();
let tool_name = tool_name.clone();
move |settings: &mut AgentSettingsContent, _cx| {
let profiles = settings.profiles.get_or_insert_default();
move |settings, _cx| {
let profiles = settings
.agent
.get_or_insert_default()
.profiles
.get_or_insert_default();
let profile = profiles
.entry(profile_id)
.entry(profile_id.0)
.or_insert_with(|| AgentProfileContent {
name: default_profile.name.into(),
tools: default_profile.tools,
@@ -318,7 +319,7 @@ impl PickerDelegate for ToolPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let item = &self.filtered_items[ix];
let item = &self.filtered_items.get(ix)?;
match item {
PickerItem::ContextServer { server_id, .. } => Some(
div()

View File

@@ -1,7 +1,6 @@
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
use acp_thread::{AcpThread, AcpThreadEvent};
use action_log::ActionLog;
use agent::{Thread, ThreadEvent, ThreadSummary};
use agent_settings::AgentSettings;
use anyhow::Result;
use buffer_diff::DiffHunkStatus;
@@ -19,7 +18,6 @@ use gpui::{
};
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
use language_model::StopReason;
use multi_buffer::PathKey;
use project::{Project, ProjectItem, ProjectPath};
use settings::{Settings, SettingsStore};
@@ -51,34 +49,29 @@ pub struct AgentDiffPane {
#[derive(PartialEq, Eq, Clone)]
pub enum AgentDiffThread {
Native(Entity<Thread>),
AcpThread(Entity<AcpThread>),
}
impl AgentDiffThread {
fn project(&self, cx: &App) -> Entity<Project> {
match self {
AgentDiffThread::Native(thread) => thread.read(cx).project().clone(),
AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
}
}
fn action_log(&self, cx: &App) -> Entity<ActionLog> {
match self {
AgentDiffThread::Native(thread) => thread.read(cx).action_log().clone(),
AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
}
}
fn summary(&self, cx: &App) -> ThreadSummary {
fn title(&self, cx: &App) -> SharedString {
match self {
AgentDiffThread::Native(thread) => thread.read(cx).summary().clone(),
AgentDiffThread::AcpThread(thread) => ThreadSummary::Ready(thread.read(cx).title()),
AgentDiffThread::AcpThread(thread) => thread.read(cx).title(),
}
}
fn is_generating(&self, cx: &App) -> bool {
match self {
AgentDiffThread::Native(thread) => thread.read(cx).is_generating(),
AgentDiffThread::AcpThread(thread) => {
thread.read(cx).status() == acp_thread::ThreadStatus::Generating
}
@@ -87,14 +80,12 @@ impl AgentDiffThread {
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
match self {
AgentDiffThread::Native(thread) => thread.read(cx).has_pending_edit_tool_uses(),
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
}
}
fn downgrade(&self) -> WeakAgentDiffThread {
match self {
AgentDiffThread::Native(thread) => WeakAgentDiffThread::Native(thread.downgrade()),
AgentDiffThread::AcpThread(thread) => {
WeakAgentDiffThread::AcpThread(thread.downgrade())
}
@@ -102,12 +93,6 @@ impl AgentDiffThread {
}
}
impl From<Entity<Thread>> for AgentDiffThread {
fn from(entity: Entity<Thread>) -> Self {
AgentDiffThread::Native(entity)
}
}
impl From<Entity<AcpThread>> for AgentDiffThread {
fn from(entity: Entity<AcpThread>) -> Self {
AgentDiffThread::AcpThread(entity)
@@ -116,25 +101,17 @@ impl From<Entity<AcpThread>> for AgentDiffThread {
#[derive(PartialEq, Eq, Clone)]
pub enum WeakAgentDiffThread {
Native(WeakEntity<Thread>),
AcpThread(WeakEntity<AcpThread>),
}
impl WeakAgentDiffThread {
pub fn upgrade(&self) -> Option<AgentDiffThread> {
match self {
WeakAgentDiffThread::Native(weak) => weak.upgrade().map(AgentDiffThread::Native),
WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
}
}
}
impl From<WeakEntity<Thread>> for WeakAgentDiffThread {
fn from(entity: WeakEntity<Thread>) -> Self {
WeakAgentDiffThread::Native(entity)
}
}
impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
fn from(entity: WeakEntity<AcpThread>) -> Self {
WeakAgentDiffThread::AcpThread(entity)
@@ -203,10 +180,6 @@ impl AgentDiffPane {
this.update_excerpts(window, cx)
}),
match &thread {
AgentDiffThread::Native(thread) => cx
.subscribe(thread, |this, _thread, event, cx| {
this.handle_native_thread_event(event, cx)
}),
AgentDiffThread::AcpThread(thread) => cx
.subscribe(thread, |this, _thread, event, cx| {
this.handle_acp_thread_event(event, cx)
@@ -313,19 +286,13 @@ impl AgentDiffPane {
}
fn update_title(&mut self, cx: &mut Context<Self>) {
let new_title = self.thread.summary(cx).unwrap_or("Agent Changes");
let new_title = self.thread.title(cx);
if new_title != self.title {
self.title = new_title;
cx.emit(EditorEvent::TitleChanged);
}
}
fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
if let ThreadEvent::SummaryGenerated = event {
self.update_title(cx)
}
}
fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) {
if let AcpThreadEvent::TitleUpdated = event {
self.update_title(cx)
@@ -569,8 +536,8 @@ impl Item for AgentDiffPane {
}
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
let summary = self.thread.summary(cx).unwrap_or("Agent Changes");
Label::new(format!("Review: {}", summary))
let title = self.thread.title(cx);
Label::new(format!("Review: {}", title))
.color(if params.selected {
Color::Default
} else {
@@ -1339,12 +1306,6 @@ impl AgentDiff {
});
let thread_subscription = match &thread {
AgentDiffThread::Native(thread) => cx.subscribe_in(thread, window, {
let workspace = workspace.clone();
move |this, _thread, event, window, cx| {
this.handle_native_thread_event(&workspace, event, window, cx)
}
}),
AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, {
let workspace = workspace.clone();
move |this, thread, event, window, cx| {
@@ -1447,47 +1408,6 @@ impl AgentDiff {
});
}
fn handle_native_thread_event(
&mut self,
workspace: &WeakEntity<Workspace>,
event: &ThreadEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
ThreadEvent::NewRequest
| ThreadEvent::Stopped(Ok(StopReason::EndTurn))
| ThreadEvent::Stopped(Ok(StopReason::MaxTokens))
| ThreadEvent::Stopped(Ok(StopReason::Refusal))
| ThreadEvent::Stopped(Err(_))
| ThreadEvent::ShowError(_)
| ThreadEvent::CompletionCanceled => {
self.update_reviewing_editors(workspace, window, cx);
}
// intentionally being exhaustive in case we add a variant we should handle
ThreadEvent::Stopped(Ok(StopReason::ToolUse))
| ThreadEvent::StreamedCompletion
| ThreadEvent::ReceivedTextChunk
| ThreadEvent::StreamedAssistantText(_, _)
| ThreadEvent::StreamedAssistantThinking(_, _)
| ThreadEvent::StreamedToolUse { .. }
| ThreadEvent::InvalidToolInput { .. }
| ThreadEvent::MissingToolUse { .. }
| ThreadEvent::MessageAdded(_)
| ThreadEvent::MessageEdited(_)
| ThreadEvent::MessageDeleted(_)
| ThreadEvent::SummaryGenerated
| ThreadEvent::SummaryChanged
| ThreadEvent::UsePendingTools { .. }
| ThreadEvent::ToolFinished { .. }
| ThreadEvent::CheckpointChanged
| ThreadEvent::ToolConfirmationNeeded
| ThreadEvent::ToolUseLimitReached
| ThreadEvent::CancelEditing
| ThreadEvent::ProfileChanged => {}
}
}
fn handle_acp_thread_event(
&mut self,
workspace: &WeakEntity<Workspace>,
@@ -1890,16 +1810,14 @@ impl editor::Addon for EditorAgentDiffAddon {
mod tests {
use super::*;
use crate::Keep;
use agent::thread_store::{self, ThreadStore};
use acp_thread::AgentConnection as _;
use agent_settings::AgentSettings;
use assistant_tool::ToolWorkingSet;
use editor::EditorSettings;
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
use project::{FakeFs, Project};
use prompt_store::PromptBuilder;
use serde_json::json;
use settings::{Settings, SettingsStore};
use std::sync::Arc;
use std::{path::Path, rc::Rc};
use theme::ThemeSettings;
use util::path;
@@ -1912,7 +1830,6 @@ mod tests {
Project::init_settings(cx);
AgentSettings::register(cx);
prompt_store::init(cx);
thread_store::init(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
EditorSettings::register(cx);
@@ -1932,21 +1849,17 @@ mod tests {
})
.unwrap();
let prompt_store = None;
let thread_store = cx
let connection = Rc::new(acp_thread::StubAgentConnection::new());
let thread = cx
.update(|cx| {
ThreadStore::load(
project.clone(),
cx.new(|_| ToolWorkingSet::default()),
prompt_store,
Arc::new(PromptBuilder::new(None).unwrap()),
cx,
)
connection
.clone()
.new_thread(project.clone(), Path::new(path!("/test")), cx)
})
.await
.unwrap();
let thread =
AgentDiffThread::Native(thread_store.update(cx, |store, cx| store.create_thread(cx)));
let thread = AgentDiffThread::AcpThread(thread);
let action_log = cx.read(|cx| thread.action_log(cx));
let (workspace, cx) =
@@ -2069,7 +1982,6 @@ mod tests {
Project::init_settings(cx);
AgentSettings::register(cx);
prompt_store::init(cx);
thread_store::init(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
EditorSettings::register(cx);
@@ -2098,22 +2010,6 @@ mod tests {
})
.unwrap();
let prompt_store = None;
let thread_store = cx
.update(|cx| {
ThreadStore::load(
project.clone(),
cx.new(|_| ToolWorkingSet::default()),
prompt_store,
Arc::new(PromptBuilder::new(None).unwrap()),
cx,
)
})
.await
.unwrap();
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
@@ -2132,8 +2028,19 @@ mod tests {
}
});
let connection = Rc::new(acp_thread::StubAgentConnection::new());
let thread = cx
.update(|_, cx| {
connection
.clone()
.new_thread(project.clone(), Path::new(path!("/test")), cx)
})
.await
.unwrap();
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
// Set the active thread
let thread = AgentDiffThread::Native(thread);
let thread = AgentDiffThread::AcpThread(thread);
cx.update(|window, cx| {
AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
});

View File

@@ -2,10 +2,8 @@ use crate::{
ModelUsageContext,
language_model_selector::{LanguageModelSelector, language_model_selector},
};
use agent_settings::AgentSettings;
use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use language_model::{ConfiguredModel, LanguageModelRegistry};
use picker::popover_menu::PickerPopoverMenu;
use settings::update_settings_file;
use std::sync::Arc;
@@ -39,37 +37,13 @@ impl AgentModelSelector {
let provider = model.provider_id().0.to_string();
let model_id = model.id().0.to_string();
match &model_usage_context {
ModelUsageContext::Thread(thread) => {
thread.update(cx, |thread, cx| {
let registry = LanguageModelRegistry::read_global(cx);
if let Some(provider) = registry.provider(&model.provider_id())
{
thread.set_configured_model(
Some(ConfiguredModel {
provider,
model: model.clone(),
}),
cx,
);
}
});
update_settings_file::<AgentSettings>(
fs.clone(),
cx,
move |settings, _cx| {
settings.set_model(model.clone());
},
);
}
ModelUsageContext::InlineAssistant => {
update_settings_file::<AgentSettings>(
fs.clone(),
cx,
move |settings, _cx| {
settings
.set_inline_assistant_model(provider.clone(), model_id);
},
);
update_settings_file(fs.clone(), cx, move |settings, _cx| {
settings
.agent
.get_or_insert_default()
.set_inline_assistant_model(provider.clone(), model_id);
});
}
}
},

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
mod acp;
mod active_thread;
mod agent_configuration;
mod agent_diff;
mod agent_model_selector;
@@ -8,7 +7,6 @@ mod buffer_codegen;
mod context_picker;
mod context_server_configuration;
mod context_strip;
mod debug;
mod inline_assistant;
mod inline_prompt_editor;
mod language_model_selector;
@@ -16,19 +14,16 @@ mod message_editor;
mod profile_selector;
mod slash_command;
mod slash_command_picker;
mod slash_command_settings;
mod terminal_codegen;
mod terminal_inline_assistant;
mod text_thread_editor;
mod thread_history;
mod tool_compatibility;
mod ui;
use std::rc::Rc;
use std::sync::Arc;
use agent::{Thread, ThreadId};
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
use agent::ThreadId;
use agent_settings::{AgentProfileId, AgentSettings};
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
@@ -44,17 +39,14 @@ use project::agent_server_store::AgentServerCommand;
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
use settings::{LanguageModelSelection, Settings as _, SettingsStore};
use std::any::TypeId;
pub use crate::active_thread::ActiveThread;
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant;
use crate::slash_command_settings::SlashCommandSettings;
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
pub use ui::preview::{all_agent_previews, get_agent_preview};
use zed_actions;
actions!(
@@ -235,14 +227,12 @@ impl ManageProfiles {
#[derive(Clone)]
pub(crate) enum ModelUsageContext {
Thread(Entity<Thread>),
InlineAssistant,
}
impl ModelUsageContext {
pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
match self {
Self::Thread(thread) => thread.read(cx).configured_model(),
Self::InlineAssistant => {
LanguageModelRegistry::read_global(cx).inline_assistant_model()
}
@@ -265,7 +255,6 @@ pub fn init(
cx: &mut App,
) {
AgentSettings::register(cx);
SlashCommandSettings::register(cx);
assistant_context::init(client.clone(), cx);
rules_library::init(cx);
@@ -421,8 +410,6 @@ fn register_slash_commands(cx: &mut App) {
slash_command_registry.register_command(assistant_slash_commands::DeltaSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::OutlineSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::TabSlashCommand, true);
slash_command_registry
.register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
@@ -442,21 +429,4 @@ fn register_slash_commands(cx: &mut App) {
}
})
.detach();
update_slash_commands_from_settings(cx);
cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
.detach();
}
fn update_slash_commands_from_settings(cx: &mut App) {
let slash_command_registry = SlashCommandRegistry::global(cx);
let settings = SlashCommandSettings::get_global(cx);
if settings.cargo_workspace.enabled {
slash_command_registry
.register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);
} else {
slash_command_registry
.unregister_command(assistant_slash_commands::CargoWorkspaceSlashCommand);
}
}

View File

@@ -6,7 +6,7 @@ pub(crate) mod symbol_context_picker;
pub(crate) mod thread_context_picker;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Result, anyhow};
@@ -23,9 +23,8 @@ use gpui::{
};
use language::Buffer;
use multi_buffer::MultiBufferRow;
use paths::contexts_dir;
use project::{Entry, ProjectPath};
use prompt_store::{PromptStore, UserPromptId};
use project::ProjectPath;
use prompt_store::PromptStore;
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
use symbol_context_picker::SymbolContextPicker;
use thread_context_picker::{
@@ -34,10 +33,8 @@ use thread_context_picker::{
use ui::{
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
};
use uuid::Uuid;
use workspace::{Workspace, notifications::NotifyResultExt};
use crate::AgentPanel;
use agent::{
ThreadId,
context::RULES_ICON,
@@ -664,7 +661,7 @@ pub(crate) fn recent_context_picker_entries(
text_thread_store: Option<WeakEntity<TextThreadStore>>,
workspace: Entity<Workspace>,
exclude_paths: &HashSet<PathBuf>,
exclude_threads: &HashSet<ThreadId>,
_exclude_threads: &HashSet<ThreadId>,
cx: &App,
) -> Vec<RecentEntry> {
let mut recent = Vec::with_capacity(6);
@@ -690,19 +687,13 @@ pub(crate) fn recent_context_picker_entries(
}),
);
let active_thread_id = workspace
.panel::<AgentPanel>(cx)
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
if let Some((thread_store, text_thread_store)) = thread_store
.and_then(|store| store.upgrade())
.zip(text_thread_store.and_then(|store| store.upgrade()))
{
let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
.filter(|(_, thread)| match thread {
ThreadContextEntry::Thread { id, .. } => {
Some(id) != active_thread_id && !exclude_threads.contains(id)
}
ThreadContextEntry::Thread { .. } => false,
ThreadContextEntry::Context { .. } => true,
})
.collect::<Vec<_>>();
@@ -874,15 +865,7 @@ fn fold_toggle(
}
}
pub enum MentionLink {
File(ProjectPath, Entry),
Symbol(ProjectPath, String),
Selection(ProjectPath, Range<usize>),
Fetch(String),
Thread(ThreadId),
TextThread(Arc<Path>),
Rule(UserPromptId),
}
pub struct MentionLink;
impl MentionLink {
const FILE: &str = "@file";
@@ -894,17 +877,6 @@ impl MentionLink {
const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
const SEPARATOR: &str = ":";
pub fn is_valid(url: &str) -> bool {
url.starts_with(Self::FILE)
|| url.starts_with(Self::SYMBOL)
|| url.starts_with(Self::FETCH)
|| url.starts_with(Self::SELECTION)
|| url.starts_with(Self::THREAD)
|| url.starts_with(Self::RULE)
}
pub fn for_file(file_name: &str, full_path: &str) -> String {
format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
}
@@ -958,75 +930,4 @@ impl MentionLink {
pub fn for_rule(rule: &RulesContextEntry) -> String {
format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
}
pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
fn extract_project_path_from_link(
path: &str,
workspace: &Entity<Workspace>,
cx: &App,
) -> Option<ProjectPath> {
let path = PathBuf::from(path);
let worktree_name = path.iter().next()?;
let path: PathBuf = path.iter().skip(1).collect();
let worktree_id = workspace
.read(cx)
.visible_worktrees(cx)
.find(|worktree| worktree.read(cx).root_name() == worktree_name)
.map(|worktree| worktree.read(cx).id())?;
Some(ProjectPath {
worktree_id,
path: path.into(),
})
}
let (prefix, argument) = link.split_once(Self::SEPARATOR)?;
match prefix {
Self::FILE => {
let project_path = extract_project_path_from_link(argument, workspace, cx)?;
let entry = workspace
.read(cx)
.project()
.read(cx)
.entry_for_path(&project_path, cx)?
.clone();
Some(MentionLink::File(project_path, entry))
}
Self::SYMBOL => {
let (path, symbol) = argument.split_once(Self::SEPARATOR)?;
let project_path = extract_project_path_from_link(path, workspace, cx)?;
Some(MentionLink::Symbol(project_path, symbol.to_string()))
}
Self::SELECTION => {
let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
let project_path = extract_project_path_from_link(path, workspace, cx)?;
let line_range = {
let (start, end) = line_args
.trim_start_matches('(')
.trim_end_matches(')')
.split_once('-')?;
start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
};
Some(MentionLink::Selection(project_path, line_range))
}
Self::THREAD => {
if let Some(encoded_filename) = argument.strip_prefix(Self::TEXT_THREAD_URL_PREFIX)
{
let filename = urlencoding::decode(encoded_filename).ok()?;
let path = contexts_dir().join(filename.as_ref()).into();
Some(MentionLink::TextThread(path))
} else {
let thread_id = ThreadId::from(argument);
Some(MentionLink::Thread(thread_id))
}
}
Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
Self::RULE => {
let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
Some(MentionLink::Rule(prompt_id))
}
_ => None,
}
}
}

View File

@@ -596,11 +596,12 @@ impl ContextPickerCompletionProvider {
file_name.to_string()
};
let path = Path::new(&full_path);
let crease_icon_path = if is_directory {
FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
FileIcons::get_folder_icon(false, path, cx)
.unwrap_or_else(|| IconName::Folder.path().into())
} else {
FileIcons::get_icon(Path::new(&full_path), cx)
.unwrap_or_else(|| IconName::File.path().into())
FileIcons::get_icon(path, cx).unwrap_or_else(|| IconName::File.path().into())
};
let completion_icon_path = if is_recent {
IconName::HistoryRerun.path().into()
@@ -742,15 +743,15 @@ impl CompletionProvider for ContextPickerCompletionProvider {
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
let state = buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next()?;
MentionCompletion::try_parse(line, offset_to_line)
});
let Some(state) = state else {
let snapshot = buffer.read(cx).snapshot();
let position = buffer_position.to_point(&snapshot);
let line_start = Point::new(position.row, 0);
let offset_to_line = snapshot.point_to_offset(line_start);
let mut lines = snapshot.text_for_range(line_start..position).lines();
let Some(line) = lines.next() else {
return Task::ready(Ok(Vec::new()));
};
let Some(state) = MentionCompletion::try_parse(line, offset_to_line) else {
return Task::ready(Ok(Vec::new()));
};
@@ -760,7 +761,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
return Task::ready(Ok(Vec::new()));
};
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end);

View File

@@ -160,7 +160,7 @@ impl PickerDelegate for FileContextPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let FileMatch { mat, .. } = &self.matches[ix];
let FileMatch { mat, .. } = &self.matches.get(ix)?;
Some(
ListItem::new(ix)
@@ -330,7 +330,7 @@ pub fn render_file_context_entry(
});
let file_icon = if is_directory {
FileIcons::get_folder_icon(false, cx)
FileIcons::get_folder_icon(false, path, cx)
} else {
FileIcons::get_icon(path, cx)
}

View File

@@ -146,7 +146,7 @@ impl PickerDelegate for RulesContextPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
let thread = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),

View File

@@ -169,7 +169,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
let mat = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),

View File

@@ -220,7 +220,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
let thread = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),

View File

@@ -5,7 +5,6 @@ use extension::ExtensionManifest;
use fs::Fs;
use gpui::WeakEntity;
use language::LanguageRegistry;
use project::project_settings::ProjectSettings;
use settings::update_settings_file;
use ui::prelude::*;
use util::ResultExt;
@@ -69,8 +68,9 @@ fn remove_context_server_settings(
fs: Arc<dyn Fs>,
cx: &mut App,
) {
update_settings_file::<ProjectSettings>(fs, cx, move |settings, _| {
update_settings_file(fs, cx, move |settings, _| {
settings
.project
.context_servers
.retain(|server_id, _| !context_server_ids.contains(server_id));
});

View File

@@ -12,16 +12,19 @@ use agent::{
};
use collections::HashSet;
use editor::Editor;
use file_icons::FileIcons;
use gpui::{
App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
Subscription, WeakEntity,
Subscription, Task, WeakEntity,
};
use itertools::Itertools;
use project::ProjectItem;
use std::{path::Path, rc::Rc};
use rope::Point;
use std::rc::Rc;
use text::ToPoint as _;
use ui::{PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use util::ResultExt as _;
use workspace::Workspace;
use zed_actions::assistant::OpenRulesLibrary;
pub struct ContextStrip {
context_store: Entity<ContextStore>,
@@ -121,38 +124,10 @@ impl ContextStrip {
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
match self.suggest_context_kind {
SuggestContextKind::File => self.suggested_file(cx),
SuggestContextKind::Thread => self.suggested_thread(cx),
}
}
fn suggested_file(&self, cx: &App) -> Option<SuggestedContext> {
let workspace = self.workspace.upgrade()?;
let active_item = workspace.read(cx).active_item(cx)?;
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
let active_buffer = active_buffer_entity.read(cx);
let project_path = active_buffer.project_path(cx)?;
if self
.context_store
.read(cx)
.file_path_included(&project_path, cx)
.is_some()
{
return None;
}
let file_name = active_buffer.file()?.file_name(cx);
let icon_path = FileIcons::get_icon(Path::new(&file_name), cx);
Some(SuggestedContext::File {
name: file_name.to_string_lossy().into_owned().into(),
buffer: active_buffer_entity.downgrade(),
icon_path,
})
}
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
if !self.context_picker.read(cx).allow_threads() {
return None;
@@ -161,24 +136,7 @@ impl ContextStrip {
let workspace = self.workspace.upgrade()?;
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
if let Some(active_thread) = panel.active_thread(cx) {
let weak_active_thread = active_thread.downgrade();
let active_thread = active_thread.read(cx);
if self
.context_store
.read(cx)
.includes_thread(active_thread.id())
{
return None;
}
Some(SuggestedContext::Thread {
name: active_thread.summary().or_default(),
thread: weak_active_thread,
})
} else if let Some(active_context_editor) = panel.active_context_editor() {
if let Some(active_context_editor) = panel.active_context_editor() {
let context = active_context_editor.read(cx).context();
let weak_context = context.downgrade();
let context = context.read(cx);
@@ -328,7 +286,75 @@ impl ContextStrip {
return;
};
crate::active_thread::open_context(context, workspace, window, cx);
match context {
AgentContextHandle::File(file_context) => {
if let Some(project_path) = file_context.project_path(cx) {
workspace.update(cx, |workspace, cx| {
workspace
.open_path(project_path, None, true, window, cx)
.detach_and_log_err(cx);
});
}
}
AgentContextHandle::Directory(directory_context) => {
let entry_id = directory_context.entry_id;
workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |_project, cx| {
cx.emit(project::Event::RevealInProjectPanel(entry_id));
})
})
}
AgentContextHandle::Symbol(symbol_context) => {
let buffer = symbol_context.buffer.read(cx);
if let Some(project_path) = buffer.project_path(cx) {
let snapshot = buffer.snapshot();
let target_position = symbol_context.range.start.to_point(&snapshot);
open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
}
}
AgentContextHandle::Selection(selection_context) => {
let buffer = selection_context.buffer.read(cx);
if let Some(project_path) = buffer.project_path(cx) {
let snapshot = buffer.snapshot();
let target_position = selection_context.range.start.to_point(&snapshot);
open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
}
}
AgentContextHandle::FetchedUrl(fetched_url_context) => {
cx.open_url(&fetched_url_context.url);
}
AgentContextHandle::Thread(_thread_context) => {}
AgentContextHandle::TextThread(text_thread_context) => {
workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
let context = text_thread_context.context.clone();
window.defer(cx, move |window, cx| {
panel.update(cx, |panel, cx| {
panel.open_prompt_editor(context, window, cx)
});
});
}
})
}
AgentContextHandle::Rules(rules_context) => window.dispatch_action(
Box::new(OpenRulesLibrary {
prompt_to_select: Some(rules_context.prompt_id.0),
}),
cx,
),
AgentContextHandle::Image(_) => {}
}
}
fn remove_focused_context(
@@ -569,6 +595,31 @@ pub enum ContextStripEvent {
impl EventEmitter<ContextStripEvent> for ContextStrip {}
pub enum SuggestContextKind {
File,
Thread,
}
fn open_editor_at_position(
project_path: project::ProjectPath,
target_position: Point,
workspace: &Entity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<()> {
let open_task = workspace.update(cx, |workspace, cx| {
workspace.open_path(project_path, None, true, window, cx)
});
window.spawn(cx, async move |cx| {
if let Some(active_editor) = open_task
.await
.log_err()
.and_then(|item| item.downcast::<Editor>())
{
active_editor
.downgrade()
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(target_position, window, cx);
})
.log_err();
}
})
}

View File

@@ -1,124 +0,0 @@
#![allow(unused, dead_code)]
use client::{ModelRequestUsage, RequestUsage};
use cloud_llm_client::{Plan, UsageLimit};
use gpui::Global;
use std::ops::{Deref, DerefMut};
use ui::prelude::*;
/// Debug only: Used for testing various account states
///
/// Use this by initializing it with
/// `cx.set_global(DebugAccountState::default());` somewhere
///
/// Then call `cx.debug_account()` to get access
#[derive(Clone, Debug)]
pub struct DebugAccountState {
pub enabled: bool,
pub trial_expired: bool,
pub plan: Plan,
pub custom_prompt_usage: ModelRequestUsage,
pub usage_based_billing_enabled: bool,
pub monthly_spending_cap: i32,
pub custom_edit_prediction_usage: UsageLimit,
}
impl DebugAccountState {
pub fn enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool) -> &mut Self {
self.enabled = enabled;
self
}
pub fn set_trial_expired(&mut self, trial_expired: bool) -> &mut Self {
self.trial_expired = trial_expired;
self
}
pub fn set_plan(&mut self, plan: Plan) -> &mut Self {
self.plan = plan;
self
}
pub fn set_custom_prompt_usage(&mut self, custom_prompt_usage: ModelRequestUsage) -> &mut Self {
self.custom_prompt_usage = custom_prompt_usage;
self
}
pub fn set_usage_based_billing_enabled(
&mut self,
usage_based_billing_enabled: bool,
) -> &mut Self {
self.usage_based_billing_enabled = usage_based_billing_enabled;
self
}
pub fn set_monthly_spending_cap(&mut self, monthly_spending_cap: i32) -> &mut Self {
self.monthly_spending_cap = monthly_spending_cap;
self
}
pub fn set_custom_edit_prediction_usage(
&mut self,
custom_edit_prediction_usage: UsageLimit,
) -> &mut Self {
self.custom_edit_prediction_usage = custom_edit_prediction_usage;
self
}
}
impl Default for DebugAccountState {
fn default() -> Self {
Self {
enabled: false,
trial_expired: false,
plan: Plan::ZedFree,
custom_prompt_usage: ModelRequestUsage(RequestUsage {
limit: UsageLimit::Unlimited,
amount: 0,
}),
usage_based_billing_enabled: false,
// $50.00
monthly_spending_cap: 5000,
custom_edit_prediction_usage: UsageLimit::Unlimited,
}
}
}
impl DebugAccountState {
pub fn get_global(cx: &App) -> &Self {
&cx.global::<GlobalDebugAccountState>().0
}
}
#[derive(Clone, Debug)]
pub struct GlobalDebugAccountState(pub DebugAccountState);
impl Global for GlobalDebugAccountState {}
impl Deref for GlobalDebugAccountState {
type Target = DebugAccountState;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for GlobalDebugAccountState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub trait DebugAccount {
fn debug_account(&self) -> &DebugAccountState;
}
impl DebugAccount for App {
fn debug_account(&self) -> &DebugAccountState {
&self.global::<GlobalDebugAccountState>().0
}
}

View File

@@ -744,19 +744,14 @@ impl InlineAssistant {
.update(cx, |editor, cx| {
let scroll_top = editor.scroll_position(cx).y;
let scroll_bottom = scroll_top + editor.visible_line_count().unwrap_or(0.);
let prompt_row = editor
editor_assists.scroll_lock = editor
.row_for_block(decorations.prompt_block_id, cx)
.unwrap()
.0 as f32;
if (scroll_top..scroll_bottom).contains(&prompt_row) {
editor_assists.scroll_lock = Some(InlineAssistScrollLock {
.map(|row| row.0 as f32)
.filter(|prompt_row| (scroll_top..scroll_bottom).contains(&prompt_row))
.map(|prompt_row| InlineAssistScrollLock {
assist_id,
distance_from_top: prompt_row - scroll_top,
});
} else {
editor_assists.scroll_lock = None;
}
})
.ok();
}
@@ -917,14 +912,12 @@ impl InlineAssistant {
editor.update(cx, |editor, cx| {
let scroll_position = editor.scroll_position(cx);
let target_scroll_top = editor
.row_for_block(decorations.prompt_block_id, cx)
.unwrap()
.0 as f32
let target_scroll_top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32
- scroll_lock.distance_from_top;
if target_scroll_top != scroll_position.y {
editor.set_scroll_position(point(scroll_position.x, target_scroll_top), window, cx);
}
Some(())
});
}
@@ -968,14 +961,14 @@ impl InlineAssistant {
if let Some(decorations) = assist.decorations.as_ref() {
let distance_from_top = editor.update(cx, |editor, cx| {
let scroll_top = editor.scroll_position(cx).y;
let prompt_row = editor
.row_for_block(decorations.prompt_block_id, cx)
.unwrap()
.0 as f32;
prompt_row - scroll_top
let prompt_row =
editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32;
Some(prompt_row - scroll_top)
});
if distance_from_top != scroll_lock.distance_from_top {
if distance_from_top.is_none_or(|distance_from_top| {
distance_from_top != scroll_lock.distance_from_top
}) {
editor_assists.scroll_lock = None;
}
}
@@ -1813,16 +1806,13 @@ impl CodeActionProvider for AssistantCodeActionProvider {
has_diagnostics = true;
}
if has_diagnostics {
if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None)
&& let Some(symbol) = symbols_containing_start.last()
{
let symbols_containing_start = snapshot.symbols_containing(range.start, None);
if let Some(symbol) = symbols_containing_start.last() {
range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
}
if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None)
&& let Some(symbol) = symbols_containing_end.last()
{
let symbols_containing_end = snapshot.symbols_containing(range.end, None);
if let Some(symbol) = symbols_containing_end.last() {
range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
}

View File

@@ -11,8 +11,8 @@ use editor::{
};
use fs::Fs;
use gpui::{
AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable,
Subscription, TextStyle, WeakEntity, Window,
AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
Focusable, Subscription, TextStyle, WeakEntity, Window,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use parking_lot::Mutex;
@@ -229,7 +229,7 @@ impl<T: 'static> PromptEditor<T> {
self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text("Add a prompt…", cx);
editor.set_placeholder_text("Add a prompt…", window, cx);
editor.set_text(prompt, window, cx);
insert_message_creases(
&mut editor,
@@ -272,7 +272,31 @@ impl<T: 'static> PromptEditor<T> {
}
fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
crate::active_thread::attach_pasted_images_as_context(&self.context_store, cx);
let images = cx
.read_from_clipboard()
.map(|item| {
item.into_entries()
.filter_map(|entry| {
if let ClipboardEntry::Image(image) = entry {
Some(image)
} else {
None
}
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if images.is_empty() {
return;
}
cx.stop_propagation();
self.context_store.update(cx, |store, cx| {
for image in images {
store.add_image_instance(Arc::new(image), cx);
}
});
}
fn handle_prompt_editor_events(
@@ -782,7 +806,7 @@ impl PromptEditor<BufferCodegen> {
// always show the cursor (even when it isn't focused) because
// typing in one will make what you typed appear in all of them.
editor.set_show_cursor_when_unfocused(true, cx);
editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
editor.register_addon(ContextCreasesAddon::new());
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
@@ -949,7 +973,7 @@ impl PromptEditor<TerminalCodegen> {
cx,
);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,14 @@
use crate::{ManageProfiles, ToggleProfileSelector};
use agent::agent_profile::{AgentProfile, AvailableProfiles};
use agent_settings::{AgentDockPosition, AgentProfileId, AgentSettings, builtin_profiles};
use agent_settings::{
AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, builtin_profiles,
};
use fs::Fs;
use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*};
use settings::{Settings as _, SettingsStore, update_settings_file};
use settings::{DockPosition, Settings as _, SettingsStore, update_settings_file};
use std::sync::Arc;
use ui::{
ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, TintColor,
Tooltip, prelude::*,
ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, PopoverMenu,
PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
/// Trait for types that can provide and manage agent profiles
@@ -127,9 +128,11 @@ impl ProfileSelector {
.toggleable(IconPosition::End, profile_id == thread_profile_id);
let entry = if let Some(doc_text) = documentation {
entry.documentation_aside(documentation_side(settings.dock), move |_| {
Label::new(doc_text).into_any_element()
})
entry.documentation_aside(
documentation_side(settings.dock),
DocumentationEdge::Top,
move |_| Label::new(doc_text).into_any_element(),
)
} else {
entry
};
@@ -138,10 +141,13 @@ impl ProfileSelector {
let fs = self.fs.clone();
let provider = self.provider.clone();
move |_window, cx| {
update_settings_file::<AgentSettings>(fs.clone(), cx, {
update_settings_file(fs.clone(), cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings.set_profile(profile_id);
settings
.agent
.get_or_insert_default()
.set_profile(profile_id.0);
}
});
@@ -212,10 +218,10 @@ impl Render for ProfileSelector {
}
}
fn documentation_side(position: AgentDockPosition) -> DocumentationSide {
fn documentation_side(position: DockPosition) -> DocumentationSide {
match position {
AgentDockPosition::Left => DocumentationSide::Right,
AgentDockPosition::Bottom => DocumentationSide::Left,
AgentDockPosition::Right => DocumentationSide::Left,
DockPosition::Left => DocumentationSide::Right,
DockPosition::Bottom => DocumentationSide::Left,
DockPosition::Right => DocumentationSide::Left,
}
}

View File

@@ -1,37 +0,0 @@
use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
/// Settings for slash commands.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
#[settings_key(key = "slash_commands")]
pub struct SlashCommandSettings {
/// Settings for the `/cargo-workspace` slash command.
#[serde(default)]
pub cargo_workspace: CargoWorkspaceCommandSettings,
}
/// Settings for the `/cargo-workspace` slash command.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
pub struct CargoWorkspaceCommandSettings {
/// Whether `/cargo-workspace` is enabled.
#[serde(default)]
pub enabled: bool,
}
impl Settings for SlashCommandSettings {
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
SettingsSources::<Self::FileContent>::json_merge_with(
[sources.default]
.into_iter()
.chain(sources.user)
.chain(sources.server),
)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -3,7 +3,7 @@ use crate::{
language_model_selector::{LanguageModelSelector, language_model_selector},
ui::BurnModeTooltip,
};
use agent_settings::{AgentSettings, CompletionMode};
use agent_settings::CompletionMode;
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
@@ -41,7 +41,10 @@ use project::{Project, Worktree};
use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
use rope::Point;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore, update_settings_file};
use settings::{
LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore,
update_settings_file,
};
use std::{
any::TypeId,
cmp,
@@ -294,11 +297,16 @@ impl TextThreadEditor {
language_model_selector(
|cx| LanguageModelRegistry::read_global(cx).default_model(),
move |model, cx| {
update_settings_file::<AgentSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
update_settings_file(fs.clone(), cx, move |settings, _| {
let provider = model.provider_id().0.to_string();
let model = model.id().0.to_string();
settings.agent.get_or_insert_default().set_model(
LanguageModelSelection {
provider: LanguageModelProviderSetting(provider),
model,
},
)
});
},
window,
cx,
@@ -477,7 +485,7 @@ impl TextThreadEditor {
return;
}
let selections = self.editor.read(cx).selections.disjoint_anchors();
let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
let mut commands_by_range = HashMap::default();
let workspace = self.workspace.clone();
self.context.update(cx, |context, cx| {
@@ -1823,7 +1831,7 @@ impl TextThreadEditor {
fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context<Self>) {
self.context.update(cx, |context, cx| {
let selections = self.editor.read(cx).selections.disjoint_anchors();
let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
for selection in selections.as_ref() {
let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
let range = selection

View File

@@ -1,912 +0,0 @@
use crate::{AgentPanel, RemoveSelectedThread};
use agent::history_store::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
UniformListScrollHandle, WeakEntity, Window, uniform_list,
};
use std::{fmt::Display, ops::Range, sync::Arc};
use time::{OffsetDateTime, UtcOffset};
use ui::{
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
Tooltip, prelude::*,
};
use util::ResultExt;
pub struct ThreadHistory {
agent_panel: WeakEntity<AgentPanel>,
history_store: Entity<HistoryStore>,
scroll_handle: UniformListScrollHandle,
selected_index: usize,
hovered_index: Option<usize>,
search_editor: Entity<Editor>,
all_entries: Arc<Vec<HistoryEntry>>,
// When the search is empty, we display date separators between history entries
// This vector contains an enum of either a separator or an actual entry
separated_items: Vec<ListItemType>,
// Maps entry indexes to list item indexes
separated_item_indexes: Vec<u32>,
_separated_items_task: Option<Task<()>>,
search_state: SearchState,
scrollbar_visibility: bool,
scrollbar_state: ScrollbarState,
_subscriptions: Vec<gpui::Subscription>,
}
enum SearchState {
Empty,
Searching {
query: SharedString,
_task: Task<()>,
},
Searched {
query: SharedString,
matches: Vec<StringMatch>,
},
}
enum ListItemType {
BucketSeparator(TimeBucket),
Entry {
index: usize,
format: EntryTimeFormat,
},
}
impl ListItemType {
fn entry_index(&self) -> Option<usize> {
match self {
ListItemType::BucketSeparator(_) => None,
ListItemType::Entry { index, .. } => Some(*index),
}
}
}
impl ThreadHistory {
pub(crate) fn new(
agent_panel: WeakEntity<AgentPanel>,
history_store: Entity<HistoryStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let search_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_placeholder_text("Search threads...", cx);
editor
});
let search_editor_subscription =
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
if let EditorEvent::BufferEdited = event {
let query = search_editor.read(cx).text(cx);
this.search(query.into(), cx);
}
});
let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
this.update_all_entries(cx);
});
let scroll_handle = UniformListScrollHandle::default();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
agent_panel,
history_store,
scroll_handle,
selected_index: 0,
hovered_index: None,
search_state: SearchState::Empty,
all_entries: Default::default(),
separated_items: Default::default(),
separated_item_indexes: Default::default(),
search_editor,
scrollbar_visibility: true,
scrollbar_state,
_subscriptions: vec![search_editor_subscription, history_store_subscription],
_separated_items_task: None,
};
this.update_all_entries(cx);
this
}
fn update_all_entries(&mut self, cx: &mut Context<Self>) {
let new_entries: Arc<Vec<HistoryEntry>> = self
.history_store
.update(cx, |store, cx| store.entries(cx))
.into();
self._separated_items_task.take();
let mut items = Vec::with_capacity(new_entries.len() + 1);
let mut indexes = Vec::with_capacity(new_entries.len() + 1);
let bg_task = cx.background_spawn(async move {
let mut bucket = None;
let today = Local::now().naive_local().date();
for (index, entry) in new_entries.iter().enumerate() {
let entry_date = entry
.updated_at()
.with_timezone(&Local)
.naive_local()
.date();
let entry_bucket = TimeBucket::from_dates(today, entry_date);
if Some(entry_bucket) != bucket {
bucket = Some(entry_bucket);
items.push(ListItemType::BucketSeparator(entry_bucket));
}
indexes.push(items.len() as u32);
items.push(ListItemType::Entry {
index,
format: entry_bucket.into(),
});
}
(new_entries, items, indexes)
});
let task = cx.spawn(async move |this, cx| {
let (new_entries, items, indexes) = bg_task.await;
this.update(cx, |this, cx| {
let previously_selected_entry =
this.all_entries.get(this.selected_index).map(|e| e.id());
this.all_entries = new_entries;
this.separated_items = items;
this.separated_item_indexes = indexes;
match &this.search_state {
SearchState::Empty => {
if this.selected_index >= this.all_entries.len() {
this.set_selected_entry_index(
this.all_entries.len().saturating_sub(1),
cx,
);
} else if let Some(prev_id) = previously_selected_entry
&& let Some(new_ix) = this
.all_entries
.iter()
.position(|probe| probe.id() == prev_id)
{
this.set_selected_entry_index(new_ix, cx);
}
}
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
this.search(query.clone(), cx);
}
}
cx.notify();
})
.log_err();
});
self._separated_items_task = Some(task);
}
fn search(&mut self, query: SharedString, cx: &mut Context<Self>) {
if query.is_empty() {
self.search_state = SearchState::Empty;
cx.notify();
return;
}
let all_entries = self.all_entries.clone();
let fuzzy_search_task = cx.background_spawn({
let query = query.clone();
let executor = cx.background_executor().clone();
async move {
let mut candidates = Vec::with_capacity(all_entries.len());
for (idx, entry) in all_entries.iter().enumerate() {
match entry {
HistoryEntry::Thread(thread) => {
candidates.push(StringMatchCandidate::new(idx, &thread.summary));
}
HistoryEntry::Context(context) => {
candidates.push(StringMatchCandidate::new(idx, &context.title));
}
}
}
const MAX_MATCHES: usize = 100;
fuzzy::match_strings(
&candidates,
&query,
false,
true,
MAX_MATCHES,
&Default::default(),
executor,
)
.await
}
});
let task = cx.spawn({
let query = query.clone();
async move |this, cx| {
let matches = fuzzy_search_task.await;
this.update(cx, |this, cx| {
let SearchState::Searching {
query: current_query,
_task,
} = &this.search_state
else {
return;
};
if &query == current_query {
this.search_state = SearchState::Searched {
query: query.clone(),
matches,
};
this.set_selected_entry_index(0, cx);
cx.notify();
};
})
.log_err();
}
});
self.search_state = SearchState::Searching { query, _task: task };
cx.notify();
}
fn matched_count(&self) -> usize {
match &self.search_state {
SearchState::Empty => self.all_entries.len(),
SearchState::Searching { .. } => 0,
SearchState::Searched { matches, .. } => matches.len(),
}
}
fn list_item_count(&self) -> usize {
match &self.search_state {
SearchState::Empty => self.separated_items.len(),
SearchState::Searching { .. } => 0,
SearchState::Searched { matches, .. } => matches.len(),
}
}
fn search_produced_no_matches(&self) -> bool {
match &self.search_state {
SearchState::Empty => false,
SearchState::Searching { .. } => false,
SearchState::Searched { matches, .. } => matches.is_empty(),
}
}
fn get_match(&self, ix: usize) -> Option<&HistoryEntry> {
match &self.search_state {
SearchState::Empty => self.all_entries.get(ix),
SearchState::Searching { .. } => None,
SearchState::Searched { matches, .. } => matches
.get(ix)
.and_then(|m| self.all_entries.get(m.candidate_id)),
}
}
pub fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let count = self.matched_count();
if count > 0 {
if self.selected_index == 0 {
self.set_selected_entry_index(count - 1, cx);
} else {
self.set_selected_entry_index(self.selected_index - 1, cx);
}
}
}
pub fn select_next(
&mut self,
_: &menu::SelectNext,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let count = self.matched_count();
if count > 0 {
if self.selected_index == count - 1 {
self.set_selected_entry_index(0, cx);
} else {
self.set_selected_entry_index(self.selected_index + 1, cx);
}
}
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let count = self.matched_count();
if count > 0 {
self.set_selected_entry_index(0, cx);
}
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let count = self.matched_count();
if count > 0 {
self.set_selected_entry_index(count - 1, cx);
}
}
fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) {
self.selected_index = entry_index;
let scroll_ix = match self.search_state {
SearchState::Empty | SearchState::Searching { .. } => self
.separated_item_indexes
.get(entry_index)
.map(|ix| *ix as usize)
.unwrap_or(entry_index + 1),
SearchState::Searched { .. } => entry_index,
};
self.scroll_handle
.scroll_to_item(scroll_ix, ScrollStrategy::Top);
cx.notify();
}
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
return None;
}
Some(
div()
.occlude()
.id("thread-history-scroll")
.h_full()
.bg(cx.theme().colors().panel_background.opacity(0.8))
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.absolute()
.right_1()
.top_0()
.bottom_0()
.w_4()
.pl_1()
.cursor_default()
.on_mouse_move(cx.listener(|_, _, _window, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _window, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _window, cx| {
cx.stop_propagation();
})
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry {
HistoryEntry::Thread(thread) => self.agent_panel.update(cx, move |this, cx| {
this.open_thread_by_id(&thread.id, window, cx)
}),
HistoryEntry::Context(context) => self.agent_panel.update(cx, move |this, cx| {
this.open_saved_prompt_editor(context.path.clone(), window, cx)
}),
};
if let Some(task) = task_result.log_err() {
task.detach_and_log_err(cx);
};
cx.notify();
}
}
fn remove_selected_thread(
&mut self,
_: &RemoveSelectedThread,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry {
HistoryEntry::Thread(thread) => self
.agent_panel
.update(cx, |this, cx| this.delete_thread(&thread.id, cx)),
HistoryEntry::Context(context) => self
.agent_panel
.update(cx, |this, cx| this.delete_context(context.path.clone(), cx)),
};
if let Some(task) = task_result.log_err() {
task.detach_and_log_err(cx);
};
cx.notify();
}
}
fn list_items(
&mut self,
range: Range<usize>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<AnyElement> {
let range_start = range.start;
match &self.search_state {
SearchState::Empty => self
.separated_items
.get(range)
.iter()
.flat_map(|items| {
items
.iter()
.map(|item| self.render_list_item(item.entry_index(), item, vec![], cx))
})
.collect(),
SearchState::Searched { matches, .. } => matches[range]
.iter()
.enumerate()
.map(|(ix, m)| {
self.render_list_item(
Some(range_start + ix),
&ListItemType::Entry {
index: m.candidate_id,
format: EntryTimeFormat::DateAndTime,
},
m.positions.clone(),
cx,
)
})
.collect(),
SearchState::Searching { .. } => {
vec![]
}
}
}
fn render_list_item(
&self,
list_entry_ix: Option<usize>,
item: &ListItemType,
highlight_positions: Vec<usize>,
cx: &Context<Self>,
) -> AnyElement {
match item {
ListItemType::Entry { index, format } => match self.all_entries.get(*index) {
Some(entry) => h_flex()
.w_full()
.pb_1()
.child(
HistoryEntryElement::new(entry.clone(), self.agent_panel.clone())
.highlight_positions(highlight_positions)
.timestamp_format(*format)
.selected(list_entry_ix == Some(self.selected_index))
.hovered(list_entry_ix == self.hovered_index)
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = list_entry_ix;
} else if this.hovered_index == list_entry_ix {
this.hovered_index = None;
}
cx.notify();
}))
.into_any_element(),
)
.into_any(),
None => Empty.into_any_element(),
},
ListItemType::BucketSeparator(bucket) => div()
.px(DynamicSpacing::Base06.rems(cx))
.pt_2()
.pb_1()
.child(
Label::new(bucket.to_string())
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.into_any_element(),
}
}
}
impl Focusable for ThreadHistory {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.search_editor.focus_handle(cx)
}
}
impl Render for ThreadHistory {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("ThreadHistory")
.size_full()
.bg(cx.theme().colors().panel_background)
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread))
.when(!self.all_entries.is_empty(), |parent| {
parent.child(
h_flex()
.h(px(41.)) // Match the toolbar perfectly
.w_full()
.py_1()
.px_2()
.gap_2()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
Icon::new(IconName::MagnifyingGlass)
.color(Color::Muted)
.size(IconSize::Small),
)
.child(self.search_editor.clone()),
)
})
.child({
let view = v_flex()
.id("list-container")
.relative()
.overflow_hidden()
.flex_grow();
if self.all_entries.is_empty() {
view.justify_center()
.child(
h_flex().w_full().justify_center().child(
Label::new("You don't have any past threads yet.")
.size(LabelSize::Small),
),
)
} else if self.search_produced_no_matches() {
view.justify_center().child(
h_flex().w_full().justify_center().child(
Label::new("No threads match your search.").size(LabelSize::Small),
),
)
} else {
view.pr_5()
.child(
uniform_list(
"thread-history",
self.list_item_count(),
cx.processor(|this, range: Range<usize>, window, cx| {
this.list_items(range, window, cx)
}),
)
.p_1()
.track_scroll(self.scroll_handle.clone())
.flex_grow(),
)
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
div.child(scrollbar)
})
}
})
}
}
#[derive(IntoElement)]
pub struct HistoryEntryElement {
entry: HistoryEntry,
agent_panel: WeakEntity<AgentPanel>,
selected: bool,
hovered: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
}
impl HistoryEntryElement {
pub fn new(entry: HistoryEntry, agent_panel: WeakEntity<AgentPanel>) -> Self {
Self {
entry,
agent_panel,
selected: false,
hovered: false,
highlight_positions: vec![],
timestamp_format: EntryTimeFormat::DateAndTime,
on_hover: Box::new(|_, _, _| {}),
}
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn hovered(mut self, hovered: bool) -> Self {
self.hovered = hovered;
self
}
pub fn highlight_positions(mut self, positions: Vec<usize>) -> Self {
self.highlight_positions = positions;
self
}
pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
self.on_hover = Box::new(on_hover);
self
}
pub fn timestamp_format(mut self, format: EntryTimeFormat) -> Self {
self.timestamp_format = format;
self
}
}
impl RenderOnce for HistoryEntryElement {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let (id, summary, timestamp) = match &self.entry {
HistoryEntry::Thread(thread) => (
thread.id.to_string(),
thread.summary.clone(),
thread.updated_at.timestamp(),
),
HistoryEntry::Context(context) => (
context.path.to_string_lossy().to_string(),
context.title.clone(),
context.mtime.timestamp(),
),
};
let thread_timestamp =
self.timestamp_format
.format_timestamp(&self.agent_panel, timestamp, cx);
ListItem::new(SharedString::from(id))
.rounded()
.toggle_state(self.selected)
.spacing(ListItemSpacing::Sparse)
.start_slot(
h_flex()
.w_full()
.gap_2()
.justify_between()
.child(
HighlightedLabel::new(summary, self.highlight_positions)
.size(LabelSize::Small)
.truncate(),
)
.child(
Label::new(thread_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.on_hover(self.on_hover)
.end_slot::<IconButton>(if self.hovered || self.selected {
Some(
IconButton::new("delete", IconName::Trash)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.on_click({
let agent_panel = self.agent_panel.clone();
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> =
match &self.entry {
HistoryEntry::Thread(thread) => {
let id = thread.id.clone();
Box::new(move |_event, _window, cx| {
agent_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx)
.detach_and_log_err(cx);
})
.ok();
})
}
HistoryEntry::Context(context) => {
let path = context.path.clone();
Box::new(move |_event, _window, cx| {
agent_panel
.update(cx, |this, cx| {
this.delete_context(path.clone(), cx)
.detach_and_log_err(cx);
})
.ok();
})
}
};
f
}),
)
} else {
None
})
.on_click({
let agent_panel = self.agent_panel.clone();
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> = match &self.entry
{
HistoryEntry::Thread(thread) => {
let id = thread.id.clone();
Box::new(move |_event, window, cx| {
agent_panel
.update(cx, |this, cx| {
this.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx);
})
.ok();
})
}
HistoryEntry::Context(context) => {
let path = context.path.clone();
Box::new(move |_event, window, cx| {
agent_panel
.update(cx, |this, cx| {
this.open_saved_prompt_editor(path.clone(), window, cx)
.detach_and_log_err(cx);
})
.ok();
})
}
};
f
})
}
}
#[derive(Clone, Copy)]
pub enum EntryTimeFormat {
DateAndTime,
TimeOnly,
}
impl EntryTimeFormat {
fn format_timestamp(
&self,
agent_panel: &WeakEntity<AgentPanel>,
timestamp: i64,
cx: &App,
) -> String {
let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
let timezone = agent_panel
.read_with(cx, |this, _cx| this.local_timezone())
.unwrap_or(UtcOffset::UTC);
match &self {
EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp(
timestamp,
OffsetDateTime::now_utc(),
timezone,
time_format::TimestampFormat::EnhancedAbsolute,
),
EntryTimeFormat::TimeOnly => time_format::format_time(timestamp),
}
}
}
impl From<TimeBucket> for EntryTimeFormat {
fn from(bucket: TimeBucket) -> Self {
match bucket {
TimeBucket::Today => EntryTimeFormat::TimeOnly,
TimeBucket::Yesterday => EntryTimeFormat::TimeOnly,
TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime,
TimeBucket::PastWeek => EntryTimeFormat::DateAndTime,
TimeBucket::All => EntryTimeFormat::DateAndTime,
}
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
enum TimeBucket {
Today,
Yesterday,
ThisWeek,
PastWeek,
All,
}
impl TimeBucket {
fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {
if date == reference {
return TimeBucket::Today;
}
if date == reference - TimeDelta::days(1) {
return TimeBucket::Yesterday;
}
let week = date.iso_week();
if reference.iso_week() == week {
return TimeBucket::ThisWeek;
}
let last_week = (reference - TimeDelta::days(7)).iso_week();
if week == last_week {
return TimeBucket::PastWeek;
}
TimeBucket::All
}
}
impl Display for TimeBucket {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TimeBucket::Today => write!(f, "Today"),
TimeBucket::Yesterday => write!(f, "Yesterday"),
TimeBucket::ThisWeek => write!(f, "This Week"),
TimeBucket::PastWeek => write!(f, "Past Week"),
TimeBucket::All => write!(f, "All"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
#[test]
fn test_time_bucket_from_dates() {
let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
let date = today;
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today);
let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap();
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday);
let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap();
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap();
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap();
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap();
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
// All: not in this week or last week
let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All);
// Test year boundary cases
let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap();
assert_eq!(
TimeBucket::from_dates(new_year, date),
TimeBucket::Yesterday
);
let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap();
assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek);
}
}

View File

@@ -1,94 +0,0 @@
use agent::{Thread, ThreadEvent};
use assistant_tool::{Tool, ToolSource};
use collections::HashMap;
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
use std::sync::Arc;
use ui::prelude::*;
pub struct IncompatibleToolsState {
cache: HashMap<LanguageModelToolSchemaFormat, Vec<Arc<dyn Tool>>>,
thread: Entity<Thread>,
_thread_subscription: Subscription,
}
impl IncompatibleToolsState {
pub fn new(thread: Entity<Thread>, cx: &mut Context<Self>) -> Self {
let _tool_working_set_subscription = cx.subscribe(&thread, |this, _, event, _| {
if let ThreadEvent::ProfileChanged = event {
this.cache.clear();
}
});
Self {
cache: HashMap::default(),
thread,
_thread_subscription: _tool_working_set_subscription,
}
}
pub fn incompatible_tools(
&mut self,
model: &Arc<dyn LanguageModel>,
cx: &App,
) -> &[Arc<dyn Tool>] {
self.cache
.entry(model.tool_input_format())
.or_insert_with(|| {
self.thread
.read(cx)
.profile()
.enabled_tools(cx)
.iter()
.filter(|(_, tool)| tool.input_schema(model.tool_input_format()).is_err())
.map(|(_, tool)| tool.clone())
.collect()
})
}
}
pub struct IncompatibleToolsTooltip {
pub incompatible_tools: Vec<Arc<dyn Tool>>,
}
impl Render for IncompatibleToolsTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
ui::tooltip_container(window, cx, |container, _, cx| {
container
.w_72()
.child(Label::new("Incompatible Tools").size(LabelSize::Small))
.child(
Label::new(
"This model is incompatible with the following tools from your MCPs:",
)
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
v_flex()
.my_1p5()
.py_0p5()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.children(
self.incompatible_tools
.iter()
.map(|tool| h_flex().gap_4().child(Label::new(tool.name()).size(LabelSize::Small)).map(|parent|
match tool.source() {
ToolSource::Native => parent,
ToolSource::ContextServer { id } => parent.child(Label::new(id).size(LabelSize::Small).color(Color::Muted)),
}
)),
),
)
.child(Label::new("What To Do Instead").size(LabelSize::Small))
.child(
Label::new(
"Every other tool continues to work with this model, but to specifically use those, switch to another model.",
)
.size(LabelSize::Small)
.color(Color::Muted),
)
})
}
}

View File

@@ -5,8 +5,8 @@ mod claude_code_onboarding_modal;
mod context_pill;
mod end_trial_upsell;
mod onboarding_modal;
pub mod preview;
mod unavailable_editing_tooltip;
mod usage_callout;
pub use acp_onboarding_modal::*;
pub use agent_notification::*;
@@ -16,3 +16,4 @@ pub use context_pill::*;
pub use end_trial_upsell::*;
pub use onboarding_modal::*;
pub use unavailable_editing_tooltip::*;
pub use usage_callout::*;

View File

@@ -13,11 +13,9 @@ use rope::Point;
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
use agent::context::{
AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext,
DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext,
ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle,
SymbolContext, SymbolContextHandle, TextThreadContext, TextThreadContextHandle, ThreadContext,
ThreadContextHandle,
AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext,
FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
};
#[derive(IntoElement)]
@@ -317,33 +315,11 @@ impl AddedContext {
}
}
pub fn new_attached(
context: &AgentContext,
model: Option<&Arc<dyn language_model::LanguageModel>>,
cx: &App,
) -> AddedContext {
match context {
AgentContext::File(context) => Self::attached_file(context, cx),
AgentContext::Directory(context) => Self::attached_directory(context),
AgentContext::Symbol(context) => Self::attached_symbol(context, cx),
AgentContext::Selection(context) => Self::attached_selection(context, cx),
AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()),
AgentContext::Thread(context) => Self::attached_thread(context),
AgentContext::TextThread(context) => Self::attached_text_thread(context),
AgentContext::Rules(context) => Self::attached_rules(context),
AgentContext::Image(context) => Self::image(context.clone(), model, cx),
}
}
fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
let full_path = handle.buffer.read(cx).file()?.full_path(cx);
Some(Self::file(handle, &full_path, cx))
}
fn attached_file(context: &FileContext, cx: &App) -> AddedContext {
Self::file(context.handle.clone(), &context.full_path, cx)
}
fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
@@ -371,10 +347,6 @@ impl AddedContext {
Some(Self::directory(handle, &full_path))
}
fn attached_directory(context: &DirectoryContext) -> AddedContext {
Self::directory(context.handle.clone(), &context.full_path)
}
fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
@@ -411,25 +383,6 @@ impl AddedContext {
})
}
fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext {
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
AddedContext {
kind: ContextKind::Symbol,
name: context.handle.symbol.clone(),
parent: Some(excerpt.file_name_and_range.clone()),
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let text = context.text.clone();
Some(Rc::new(move |_, cx| {
excerpt.hover_view(text.clone(), cx).into()
}))
},
handle: AgentContextHandle::Symbol(context.handle.clone()),
}
}
fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
Some(AddedContext {
@@ -449,25 +402,6 @@ impl AddedContext {
})
}
fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext {
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
AddedContext {
kind: ContextKind::Selection,
name: excerpt.file_name_and_range.clone(),
parent: excerpt.parent_name.clone(),
tooltip: None,
icon_path: excerpt.icon_path.clone(),
status: ContextStatus::Ready,
render_hover: {
let text = context.text.clone();
Some(Rc::new(move |_, cx| {
excerpt.hover_view(text.clone(), cx).into()
}))
},
handle: AgentContextHandle::Selection(context.handle.clone()),
}
}
fn fetched_url(context: FetchedUrlContext) -> AddedContext {
AddedContext {
kind: ContextKind::FetchedUrl,
@@ -506,24 +440,6 @@ impl AddedContext {
}
}
fn attached_thread(context: &ThreadContext) -> AddedContext {
AddedContext {
kind: ContextKind::Thread,
name: context.title.clone(),
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let text = context.text.clone();
Some(Rc::new(move |_, cx| {
ContextPillHover::new_text(text.clone(), cx).into()
}))
},
handle: AgentContextHandle::Thread(context.handle.clone()),
}
}
fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
AddedContext {
kind: ContextKind::TextThread,
@@ -543,24 +459,6 @@ impl AddedContext {
}
}
fn attached_text_thread(context: &TextThreadContext) -> AddedContext {
AddedContext {
kind: ContextKind::TextThread,
name: context.title.clone(),
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let text = context.text.clone();
Some(Rc::new(move |_, cx| {
ContextPillHover::new_text(text.clone(), cx).into()
}))
},
handle: AgentContextHandle::TextThread(context.handle.clone()),
}
}
fn pending_rules(
handle: RulesContextHandle,
prompt_store: Option<&Entity<PromptStore>>,
@@ -584,28 +482,6 @@ impl AddedContext {
})
}
fn attached_rules(context: &RulesContext) -> AddedContext {
let title = context
.title
.clone()
.unwrap_or_else(|| "Unnamed Rule".into());
AddedContext {
kind: ContextKind::Rules,
name: title,
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let text = context.text.clone();
Some(Rc::new(move |_, cx| {
ContextPillHover::new_text(text.clone(), cx).into()
}))
},
handle: AgentContextHandle::Rules(context.handle.clone()),
}
}
fn image(
context: ImageContext,
model: Option<&Arc<dyn language_model::LanguageModel>>,

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use ai_onboarding::{AgentPanelOnboardingCard, PlanDefinitions};
use client::zed_urls;
use cloud_llm_client::Plan;
use cloud_llm_client::{Plan, PlanV1};
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Divider, Tooltip, prelude::*};
@@ -112,7 +112,7 @@ impl Component for EndTrialUpsell {
Some(
v_flex()
.child(EndTrialUpsell {
plan: Plan::ZedFree,
plan: Plan::V1(PlanV1::ZedFree),
dismiss_upsell: Arc::new(|_, _| {}),
})
.into_any_element(),

View File

@@ -1,5 +0,0 @@
mod agent_preview;
mod usage_callouts;
pub use agent_preview::*;
pub use usage_callouts::*;

View File

@@ -1,89 +0,0 @@
use std::sync::OnceLock;
use collections::HashMap;
use component::ComponentId;
use gpui::{App, Entity, WeakEntity};
use ui::{AnyElement, Component, ComponentScope, Window};
use workspace::Workspace;
use crate::ActiveThread;
/// Function type for creating agent component previews
pub type PreviewFn =
fn(WeakEntity<Workspace>, Entity<ActiveThread>, &mut Window, &mut App) -> Option<AnyElement>;
pub struct AgentPreviewFn(fn() -> (ComponentId, PreviewFn));
impl AgentPreviewFn {
pub const fn new(f: fn() -> (ComponentId, PreviewFn)) -> Self {
Self(f)
}
}
inventory::collect!(AgentPreviewFn);
/// Trait that must be implemented by components that provide agent previews.
pub trait AgentPreview: Component + Sized {
#[allow(unused)] // We can't know this is used due to the distributed slice
fn scope(&self) -> ComponentScope {
ComponentScope::Agent
}
/// Static method to create a preview for this component type
fn agent_preview(
workspace: WeakEntity<Workspace>,
active_thread: Entity<ActiveThread>,
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement>;
}
/// Register an agent preview for the given component type
#[macro_export]
macro_rules! register_agent_preview {
($type:ty) => {
inventory::submit! {
$crate::ui::preview::AgentPreviewFn::new(|| {
(
<$type as component::Component>::id(),
<$type as $crate::ui::preview::AgentPreview>::agent_preview,
)
})
}
};
}
/// Lazy initialized registry of preview functions
static AGENT_PREVIEW_REGISTRY: OnceLock<HashMap<ComponentId, PreviewFn>> = OnceLock::new();
/// Initialize the agent preview registry if needed
fn get_or_init_registry() -> &'static HashMap<ComponentId, PreviewFn> {
AGENT_PREVIEW_REGISTRY.get_or_init(|| {
let mut map = HashMap::default();
for register_fn in inventory::iter::<AgentPreviewFn>() {
let (id, preview_fn) = (register_fn.0)();
map.insert(id, preview_fn);
}
map
})
}
/// Get a specific agent preview by component ID.
pub fn get_agent_preview(
id: &ComponentId,
workspace: WeakEntity<Workspace>,
active_thread: Entity<ActiveThread>,
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
let registry = get_or_init_registry();
registry
.get(id)
.and_then(|preview_fn| preview_fn(workspace, active_thread, window, cx))
}
/// Get all registered agent previews.
pub fn all_agent_previews() -> Vec<ComponentId> {
let registry = get_or_init_registry();
registry.keys().cloned().collect()
}

View File

@@ -1,5 +1,5 @@
use client::{ModelRequestUsage, RequestUsage, zed_urls};
use cloud_llm_client::{Plan, UsageLimit};
use cloud_llm_client::{Plan, PlanV1, PlanV2, UsageLimit};
use component::{empty_example, example_group_with_title, single_example};
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Callout, prelude::*};
@@ -38,20 +38,20 @@ impl RenderOnce for UsageCallout {
let (title, message, button_text, url) = if is_limit_reached {
match self.plan {
Plan::ZedFree | Plan::ZedFreeV2 => (
Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree) => (
"Out of free prompts",
"Upgrade to continue, wait for the next reset, or switch to API key."
.to_string(),
"Upgrade",
zed_urls::account_url(cx),
),
Plan::ZedProTrial | Plan::ZedProTrialV2 => (
Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial) => (
"Out of trial prompts",
"Upgrade to Zed Pro to continue, or switch to API key.".to_string(),
"Upgrade",
zed_urls::account_url(cx),
),
Plan::ZedPro | Plan::ZedProV2 => (
Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro) => (
"Out of included prompts",
"Enable usage-based billing to continue.".to_string(),
"Manage",
@@ -60,7 +60,7 @@ impl RenderOnce for UsageCallout {
}
} else {
match self.plan {
Plan::ZedFree => (
Plan::V1(PlanV1::ZedFree) => (
"Reaching free plan limit soon",
format!(
"{remaining} remaining - Upgrade to increase limit, or switch providers",
@@ -68,7 +68,7 @@ impl RenderOnce for UsageCallout {
"Upgrade",
zed_urls::account_url(cx),
),
Plan::ZedProTrial => (
Plan::V1(PlanV1::ZedProTrial) => (
"Reaching trial limit soon",
format!(
"{remaining} remaining - Upgrade to increase limit, or switch providers",
@@ -76,7 +76,7 @@ impl RenderOnce for UsageCallout {
"Upgrade",
zed_urls::account_url(cx),
),
_ => return div().into_any_element(),
Plan::V1(PlanV1::ZedPro) | Plan::V2(_) => return div().into_any_element(),
}
};
@@ -119,7 +119,7 @@ impl Component for UsageCallout {
single_example(
"Approaching limit (90%)",
UsageCallout::new(
Plan::ZedFree,
Plan::V1(PlanV1::ZedFree),
ModelRequestUsage(RequestUsage {
limit: UsageLimit::Limited(50),
amount: 45, // 90% of limit
@@ -130,7 +130,7 @@ impl Component for UsageCallout {
single_example(
"Limit reached (100%)",
UsageCallout::new(
Plan::ZedFree,
Plan::V1(PlanV1::ZedFree),
ModelRequestUsage(RequestUsage {
limit: UsageLimit::Limited(50),
amount: 50, // 100% of limit
@@ -147,7 +147,7 @@ impl Component for UsageCallout {
single_example(
"Approaching limit (90%)",
UsageCallout::new(
Plan::ZedProTrial,
Plan::V1(PlanV1::ZedProTrial),
ModelRequestUsage(RequestUsage {
limit: UsageLimit::Limited(150),
amount: 135, // 90% of limit
@@ -158,7 +158,7 @@ impl Component for UsageCallout {
single_example(
"Limit reached (100%)",
UsageCallout::new(
Plan::ZedProTrial,
Plan::V1(PlanV1::ZedProTrial),
ModelRequestUsage(RequestUsage {
limit: UsageLimit::Limited(150),
amount: 150, // 100% of limit
@@ -175,7 +175,7 @@ impl Component for UsageCallout {
single_example(
"Limit reached (100%)",
UsageCallout::new(
Plan::ZedPro,
Plan::V1(PlanV1::ZedPro),
ModelRequestUsage(RequestUsage {
limit: UsageLimit::Limited(500),
amount: 500, // 100% of limit

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use client::{Client, UserStore};
use cloud_llm_client::Plan;
use cloud_llm_client::{Plan, PlanV1, PlanV2};
use gpui::{Entity, IntoElement, ParentElement};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::prelude::*;
@@ -57,8 +57,15 @@ impl AgentPanelOnboarding {
impl Render for AgentPanelOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let enrolled_in_trial = self.user_store.read(cx).plan() == Some(Plan::ZedProTrial);
let is_pro_user = self.user_store.read(cx).plan() == Some(Plan::ZedPro);
let enrolled_in_trial = self.user_store.read(cx).plan().is_some_and(|plan| {
matches!(
plan,
Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial)
)
});
let is_pro_user = self.user_store.read(cx).plan().is_some_and(|plan| {
matches!(plan, Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro))
});
AgentPanelOnboardingCard::new()
.child(

View File

@@ -10,7 +10,7 @@ pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProvider
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
pub use ai_upsell_card::AiUpsellCard;
use cloud_llm_client::Plan;
use cloud_llm_client::{Plan, PlanV1, PlanV2};
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
pub use plan_definitions::PlanDefinitions;
pub use young_account_banner::YoungAccountBanner;
@@ -308,13 +308,13 @@ impl RenderOnce for ZedAiOnboarding {
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
match self.plan {
None => self.render_free_plan_state(cx.has_flag::<BillingV2FeatureFlag>(), cx),
Some(plan @ (Plan::ZedFree | Plan::ZedFreeV2)) => {
Some(plan @ (Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))) => {
self.render_free_plan_state(plan.is_v2(), cx)
}
Some(plan @ (Plan::ZedProTrial | Plan::ZedProTrialV2)) => {
Some(plan @ (Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial))) => {
self.render_trial_state(plan.is_v2(), cx)
}
Some(plan @ (Plan::ZedPro | Plan::ZedProV2)) => {
Some(plan @ (Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro))) => {
self.render_pro_plan_state(plan.is_v2(), cx)
}
}
@@ -370,15 +370,27 @@ impl Component for ZedAiOnboarding {
),
single_example(
"Free Plan",
onboarding(SignInStatus::SignedIn, Some(Plan::ZedFree), false),
onboarding(
SignInStatus::SignedIn,
Some(Plan::V1(PlanV1::ZedFree)),
false,
),
),
single_example(
"Pro Trial",
onboarding(SignInStatus::SignedIn, Some(Plan::ZedProTrial), false),
onboarding(
SignInStatus::SignedIn,
Some(Plan::V1(PlanV1::ZedProTrial)),
false,
),
),
single_example(
"Pro Plan",
onboarding(SignInStatus::SignedIn, Some(Plan::ZedPro), false),
onboarding(
SignInStatus::SignedIn,
Some(Plan::V1(PlanV1::ZedPro)),
false,
),
),
])
.into_any_element(),

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use cloud_llm_client::Plan;
use cloud_llm_client::{Plan, PlanV1, PlanV2};
use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt};
use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window};
use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*};
@@ -171,7 +171,7 @@ impl RenderOnce for AiUpsellCard {
match self.sign_in_status {
SignInStatus::SignedIn => match self.user_plan {
None | Some(Plan::ZedFree | Plan::ZedFreeV2) => card
None | Some(Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree)) => card
.child(Label::new("Try Zed AI").size(LabelSize::Large))
.map(|this| {
if self.account_too_young {
@@ -237,16 +237,17 @@ impl RenderOnce for AiUpsellCard {
)
}
}),
Some(plan @ (Plan::ZedProTrial | Plan::ZedProTrialV2)) => card
.child(pro_trial_stamp)
.child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
.child(
Label::new("Here's what you get for the next 14 days:")
.color(Color::Muted)
.mb_2(),
)
.child(PlanDefinitions.pro_trial(plan.is_v2(), false)),
Some(plan @ (Plan::ZedPro | Plan::ZedProV2)) => card
Some(plan @ (Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial))) => {
card.child(pro_trial_stamp)
.child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
.child(
Label::new("Here's what you get for the next 14 days:")
.color(Color::Muted)
.mb_2(),
)
.child(PlanDefinitions.pro_trial(plan.is_v2(), false))
}
Some(plan @ (Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro))) => card
.child(certified_user_stamp)
.child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large))
.child(
@@ -326,7 +327,7 @@ impl Component for AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
account_too_young: false,
user_plan: Some(Plan::ZedFree),
user_plan: Some(Plan::V1(PlanV1::ZedFree)),
tab_index: Some(1),
}
.into_any_element(),
@@ -337,7 +338,7 @@ impl Component for AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
account_too_young: true,
user_plan: Some(Plan::ZedFree),
user_plan: Some(Plan::V1(PlanV1::ZedFree)),
tab_index: Some(1),
}
.into_any_element(),
@@ -348,7 +349,7 @@ impl Component for AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
account_too_young: false,
user_plan: Some(Plan::ZedProTrial),
user_plan: Some(Plan::V1(PlanV1::ZedProTrial)),
tab_index: Some(1),
}
.into_any_element(),
@@ -359,7 +360,7 @@ impl Component for AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
account_too_young: false,
user_plan: Some(Plan::ZedPro),
user_plan: Some(Plan::V1(PlanV1::ZedPro)),
tab_index: Some(1),
}
.into_any_element(),

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use client::{Client, UserStore};
use cloud_llm_client::Plan;
use cloud_llm_client::{Plan, PlanV1, PlanV2};
use gpui::{Entity, IntoElement, ParentElement};
use ui::prelude::*;
@@ -36,7 +36,9 @@ impl EditPredictionOnboarding {
impl Render for EditPredictionOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_free_plan = self.user_store.read(cx).plan() == Some(Plan::ZedFree);
let is_free_plan = self.user_store.read(cx).plan().is_some_and(|plan| {
matches!(plan, Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))
});
let github_copilot = v_flex()
.gap_1()

View File

@@ -23,6 +23,7 @@ http_client.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
strum.workspace = true
thiserror.workspace = true
workspace-hack.workspace = true

View File

@@ -8,6 +8,7 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B
use http_client::http::{self, HeaderMap, HeaderValue};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode};
use serde::{Deserialize, Serialize};
pub use settings::{AnthropicAvailableModel as AvailableModel, ModelMode};
use strum::{EnumIter, EnumString};
use thiserror::Error;
@@ -31,6 +32,24 @@ pub enum AnthropicModelMode {
},
}
impl From<ModelMode> for AnthropicModelMode {
fn from(value: ModelMode) -> Self {
match value {
ModelMode::Default => AnthropicModelMode::Default,
ModelMode::Thinking { budget_tokens } => AnthropicModelMode::Thinking { budget_tokens },
}
}
}
impl From<AnthropicModelMode> for ModelMode {
fn from(value: AnthropicModelMode) -> Self {
match value {
AnthropicModelMode::Default => ModelMode::Default,
AnthropicModelMode::Thinking { budget_tokens } => ModelMode::Thinking { budget_tokens },
}
}
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {

View File

@@ -14,7 +14,6 @@ path = "src/assistant_slash_commands.rs"
[dependencies]
anyhow.workspace = true
assistant_slash_command.workspace = true
cargo_toml.workspace = true
chrono.workspace = true
collections.workspace = true
context_server.workspace = true
@@ -35,7 +34,6 @@ serde.workspace = true
serde_json.workspace = true
smol.workspace = true
text.workspace = true
toml.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true

View File

@@ -1,4 +1,3 @@
mod cargo_workspace_command;
mod context_server_command;
mod default_command;
mod delta_command;
@@ -12,7 +11,6 @@ mod streaming_example_command;
mod symbols_command;
mod tab_command;
pub use crate::cargo_workspace_command::*;
pub use crate::context_server_command::*;
pub use crate::default_command::*;
pub use crate::delta_command::*;

View File

@@ -1,158 +0,0 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use fs::Fs;
use gpui::{App, Entity, Task, WeakEntity};
use language::{BufferSnapshot, LspAdapterDelegate};
use project::{Project, ProjectPath};
use std::{
fmt::Write,
path::Path,
sync::{Arc, atomic::AtomicBool},
};
use ui::prelude::*;
use workspace::Workspace;
pub struct CargoWorkspaceSlashCommand;
impl CargoWorkspaceSlashCommand {
async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
let buffer = fs.load(path_to_cargo_toml).await?;
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
let mut message = String::new();
writeln!(message, "You are in a Rust project.")?;
if let Some(workspace) = cargo_toml.workspace {
writeln!(
message,
"The project is a Cargo workspace with the following members:"
)?;
for member in workspace.members {
writeln!(message, "- {member}")?;
}
if !workspace.default_members.is_empty() {
writeln!(message, "The default members are:")?;
for member in workspace.default_members {
writeln!(message, "- {member}")?;
}
}
if !workspace.dependencies.is_empty() {
writeln!(
message,
"The following workspace dependencies are installed:"
)?;
for dependency in workspace.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
} else if let Some(package) = cargo_toml.package {
writeln!(
message,
"The project name is \"{name}\".",
name = package.name
)?;
let description = package
.description
.as_ref()
.and_then(|description| description.get().ok().cloned());
if let Some(description) = description.as_ref() {
writeln!(message, "It describes itself as \"{description}\".")?;
}
if !cargo_toml.dependencies.is_empty() {
writeln!(message, "The following dependencies are installed:")?;
for dependency in cargo_toml.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
}
Ok(message)
}
fn path_to_cargo_toml(project: Entity<Project>, cx: &mut App) -> Option<Arc<Path>> {
let worktree = project.read(cx).worktrees(cx).next()?;
let worktree = worktree.read(cx);
let entry = worktree.entry_for_path("Cargo.toml")?;
let path = ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
};
Some(Arc::from(
project.read(cx).absolute_path(&path, cx)?.as_path(),
))
}
}
impl SlashCommand for CargoWorkspaceSlashCommand {
fn name(&self) -> String {
"cargo-workspace".into()
}
fn description(&self) -> String {
"insert project workspace metadata".into()
}
fn menu_text(&self) -> String {
"Insert Project Workspace Metadata".into()
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakEntity<Workspace>>,
_window: &mut Window,
_cx: &mut App,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn requires_argument(&self) -> bool {
false
}
fn run(
self: Arc<Self>,
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakEntity<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_window: &mut Window,
cx: &mut App,
) -> Task<SlashCommandResult> {
let output = workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();
let fs = workspace.project().read(cx).fs().clone();
let path = Self::path_to_cargo_toml(project, cx);
let output = cx.background_spawn(async move {
let path = path.with_context(|| "Cargo.toml not found")?;
Self::build_message(fs, &path).await
});
cx.foreground_executor().spawn(async move {
let text = output.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::FileTree,
label: "Project".into(),
metadata: None,
}],
run_commands_in_text: false,
}
.into_event_stream())
})
});
output.unwrap_or_else(|error| Task::ready(Err(error)))
}
}

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
@@ -70,9 +70,7 @@ impl SlashCommand for OutlineSlashCommand {
let path = snapshot.resolve_file_path(cx, true);
cx.background_spawn(async move {
let outline = snapshot
.outline(None)
.context("no symbols for active tab")?;
let outline = snapshot.outline(None);
let path = path.as_deref().unwrap_or(Path::new("untitled"));
let mut outline_text = format!("Symbols for {}:\n", path.display());

View File

@@ -1,10 +1,11 @@
use action_log::ActionLog;
use anyhow::{Context as _, Result};
use gpui::{AsyncApp, Entity};
use language::{OutlineItem, ParseStatus};
use language::{Buffer, OutlineItem, ParseStatus};
use project::Project;
use regex::Regex;
use std::fmt::Write;
use std::path::Path;
use text::Point;
/// For files over this size, instead of reading them (or including them in context),
@@ -41,9 +42,7 @@ pub async fn file_outline(
}
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let outline = snapshot
.outline(None)
.context("No outline information available for this file at path {path}")?;
let outline = snapshot.outline(None);
render_outline(
outline
@@ -130,3 +129,67 @@ fn render_entries(
entries_rendered
}
/// Result of getting buffer content, which can be either full content or an outline.
pub struct BufferContent {
/// The actual content (either full text or outline)
pub text: String,
/// Whether this is an outline (true) or full content (false)
pub is_outline: bool,
}
/// Returns either the full content of a buffer or its outline, depending on size.
/// For files larger than AUTO_OUTLINE_SIZE, returns an outline with a header.
/// For smaller files, returns the full content.
pub async fn get_buffer_content_or_outline(
buffer: Entity<Buffer>,
path: Option<&Path>,
cx: &AsyncApp,
) -> Result<BufferContent> {
let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?;
if file_size > AUTO_OUTLINE_SIZE {
// For large files, use outline instead of full content
// Wait until the buffer has been fully parsed, so we can read its outline
let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await?;
}
let outline_items = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
snapshot
.outline(None)
.items
.into_iter()
.map(|item| item.to_point(&snapshot))
.collect::<Vec<_>>()
})?;
let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
let text = if let Some(path) = path {
format!(
"# File outline for {} (file too large to show full content)\n\n{}",
path.display(),
outline_text
)
} else {
format!(
"# File outline (file too large to show full content)\n\n{}",
outline_text
)
};
Ok(BufferContent {
text,
is_outline: true,
})
} else {
// File is small enough, return full content
let text = buffer.read_with(cx, |buffer, _| buffer.text())?;
Ok(BufferContent {
text,
is_outline: false,
})
}
}

View File

@@ -63,7 +63,6 @@ ui.workspace = true
util.workspace = true
watch.workspace = true
web_search.workspace = true
which.workspace = true
workspace-hack.workspace = true
workspace.workspace = true

Some files were not shown because too many files have changed in this diff Show More