Compare commits

...

167 Commits

Author SHA1 Message Date
Nathan Sobo
447eb8e1c9 Checkpoint 2025-04-21 07:08:03 -06:00
Nathan Sobo
e434117018 Checkpoint: Still a failing test for concurrent tool calls.
Seems like I'm surfacing a bug in Anthropic.
2025-04-20 20:50:20 -06:00
Nathan Sobo
36271b79b3 Failing test proving we need to batch tools per message 2025-04-20 19:04:37 -06:00
Nathan Sobo
41644a53cc Checkpoint 2025-04-20 17:56:42 -06:00
Nathan Sobo
08a9c4af09 Checkpoint 2025-04-20 17:54:33 -06:00
Nathan Sobo
3187f28405 Checkpoint 2025-04-20 17:28:44 -06:00
Nathan Sobo
101f3b100f Get a basic request/reply tested in AgentThread 2025-04-20 00:41:03 -06:00
Nathan Sobo
39c8b7bf5f Add agent_thread crate
Experimental for now, I want to try really integration testing it
against the real APIs in a more "eval style", meaning embrace the
stochastic nature of it.
2025-04-20 00:17:38 -06:00
Nathan Sobo
08b41252f6 Include role in start message 2025-04-20 00:16:49 -06:00
Nathan Sobo
152bbca238 Add gpui helpers 2025-04-20 00:16:08 -06:00
Nathan Sobo
107d8ca483 Rename regex search tool to grep and accept an include glob pattern (#29100)
This PR renames the `regex_search` tool to `grep` because I think it
conveys more meaning to the model, the idea of searching the filesystem
with a regular expression. It's also one word and the model seems to be
using it effectively after some additional prompt tuning.

It also takes an include pattern to filter on the specific files we try
to search. I'd like to encourage the model to scope its searches more
aggressively, as in my testing, I'm only seeing it filter on file
extension.

Release Notes:

- N/A
2025-04-20 00:53:30 +00:00
Michael Sloan
4278d894d2 Update find_and_replace_diff_card eval example to include .rules file (#29108)
Release Notes:

- N/A
2025-04-20 00:07:44 +00:00
Michael Sloan
a91948aeb4 agent: Reorder some linux keybindings to match mac keybindings (#29107)
Release Notes:

- Made keybindings for agent panel closer to the precedence order used
on Mac. This fixes use of `enter` to add context from the menu triggered
by `@` referencing.
2025-04-20 00:01:43 +00:00
Michael Sloan
2178b36cbc Add eval worktrees and repos to file_scan_exclusions in zed project settings (#29106)
Release Notes:

- N/A
2025-04-19 23:43:54 +00:00
Michael Sloan
0fb0059b5f Switch from open-codestral-mamba to codestral-latest for default mistral model (#29104)
Couldn't find mistral cloud pricing for open-codestral-mamba on the
mistral site, but codestral-latest is newer and appears to be cheaper
based on
https://sdk.vercel.ai/playground/mistral:codestral-mamba-latest,mistral:codestral-2501

Release Notes:

- N/A
2025-04-19 23:29:36 +00:00
Michael Sloan
fbf7caf93e Default to fast model for thread summaries and titles + don't include system prompt / context / thinking segments (#29102)
* Adds a fast / cheaper model to providers and defaults thread
summarization to this model. Initial motivation for this was that
https://github.com/zed-industries/zed/pull/29099 would cause these
requests to fail when used with a thinking model. It doesn't seem
correct to use a thinking model for summarization.

* Skips system prompt, context, and thinking segments.

* If tool use is happening, allows 2 tool uses + one more agent response
before summarizing.

Downside of this is that there was potential for some prefix cache reuse
before, especially for title summarization (thread summarization omitted
tool results and so would not share a prefix for those). This seems fine
as these requests should typically be fairly small. Even for full thread
summarization, skipping all tool use / context should greatly reduce the
token use.

Release Notes:

- N/A
2025-04-19 23:26:29 +00:00
Nathan Sobo
d48152d958 Don't send dummy user text with tool results (#29099)
Previously, we were including the dummy text "Here are the tool
results." whenever reporting tool call results. I'm worried this is
adding noise and confusing the model, because the user didn't actually
say anything. This inserts an empty message to be populated later. My
preference would be something less stateful, where tool results (or
batches of them requested simultaneously) would be sent to the model as
soon as they were ready, without bothering to do this message
association dance. But for now, this seems to work.

Release Notes:

- N/A
2025-04-19 20:34:49 +00:00
Bennet Bo Fenner
bafc086d27 agent: Preserve thinking blocks between requests (#29055)
Looks like the required backend component of this was deployed.

https://github.com/zed-industries/monorepo/actions/runs/14541199197

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-04-19 20:12:03 +00:00
Smit Barmase
f737c4d01e editor: Improve selection highlights speed (#29097)
Before, we used to debounce selection highlight because it needed to
search the whole file to show gutter line highlights, etc. This
experience felt extremely laggy.

This PR introduces a new approach where:
1. We query only visible rows without debounce. The search function
itself is async and runs in a background thread, so it's not blocking
anything. With no debounce and such a small search space, highlights
feel realtime.
2. In parallel, we also query the whole file (still debounced, like
before). Once this query resolves, it updates highlights across the
file, making scrollbar markers visible.

This hybrid way gives the feeling of realtime, while keeping the same
functionality.


https://github.com/user-attachments/assets/432b65f1-89d2-4658-ad5e-048921b06a23

P.S. I have removed the user setting for custom debounce delay, because
(one) now it doesn't really make sense to configure that, and (two) the
whole logic is based on the assumption that the fast query will resolve
before the debounced query. A static debounce time makes sure of that.
Configuring it might lead to cases where the fast query resolves after
the debounced query, and we end up only seeing visible viewport
highlights.

Release Notes:

- Improved selection highlight speed.
2025-04-20 01:20:36 +05:30
Marko Kungla
8f308d835a Add zed to Flatpak config and data directories (#28952)
Closes #28944 

Release Notes:

- linux: Fixed incorrect config directory being used when Zed is
installed via Flatpak

Signed-off-by: Marko Kungla <marko.kungla@gmail.com>
2025-04-19 10:41:03 -07:00
Danilo Leal
703a68eedf docs: Add more examples of existing MCP extensions (#29090)
Also linking to the zed.dev site, which now includes a filter for MCP
(i.e., "Context Servers") servers in the Extensions page.

Release Notes:

- N/A
2025-04-19 12:09:57 -03:00
Danilo Leal
cc2fcb2f42 agent: Add item to add custom MCP server in the panel's menu (#29091)
This is based on user feedback that the Agent Panel menu was only
linking to extensions as a way to add MCP servers while we also support
adding "custom" servers, too, which don't go through the extensions
flow.

Release Notes:

- N/A
2025-04-19 12:09:50 -03:00
张小白
f0ef3110d3 gpui: Introduce PlatformKeyboardLayout trait for human-friendly keyboard layout names (#29049)
This PR adds a new `PlatformKeyboardLayout` trait with two methods:
`id(&self) -> &str` and `name(&self) -> &str`. The `id()` method returns
a unique identifier for the keyboard layout, while `name()` provides a
human-readable name. This distinction is especially important on
Windows, where the `id` and `name` can be quite different. For example,
the French layout has an `id` of `0000040C`, which is not
human-readable, whereas the `name` would simply be `French`. Currently,
the existing `keyboard_layout()` method returns what's essentially the
same as `id()` in this new design.

This PR implements the `name()` method for both Windows and macOS. On
Linux, for now, `name()` still returns the same value as `id()`.

Release Notes:

- N/A
2025-04-19 22:23:03 +08:00
Oleksiy Syvokon
0454e7a22e terminal: Add Alt+. keybinding passthrough for last-argument recall (#29088)
Alt+. is a useful terminal/readline feature that cycles through the last
arguments of previous commands in history. Unlike many other shortcuts,
it doesn't conflict with anything important globally, so it can be
safely enabled by default.

Release Notes:

- N/A
2025-04-19 14:03:13 +03:00
Michael Sloan
d88b06a5dc Simplify language model registry + only emit change events on change (#29086)
* Now only does default fallback logic in the registry

* Only emits change events when there is actually a change

Release Notes:

- N/A
2025-04-19 08:26:42 +00:00
Michael Sloan
98ceffe026 Pretty tool inputs in eval output markdown + numbered assistant messages (#29082)
Release Notes:

- N/A
2025-04-19 06:59:22 +00:00
Nathan Sobo
bab28560ef Systematically optimize agentic editing performance (#28961)
Now that we've established a proper eval in tree, this PR is reboots of
our agent loop back to a set of minimal tools and simpler prompts. We
should aim to get this branch feeling subjectively competitive with
what's on main and then merge it, and build from there.

Let's invest in our eval and use it to drive better performance of the
agent loop. How you can help: Pick an example, and then make the outcome
faster or better. It's fine to even use your own subjective judgment, as
our evaluation criteria likely need tuning as well at this point. Focus
on making the agent work better in your own subjective experience first.
Let's focus on simple/practical improvements to make this thing work
better, then determine how we can craft our judgment criteria to lock
those improvements in.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-04-19 02:47:59 +00:00
Danilo Leal
8102a16747 agent: Make copy button show while hovering the codeblock container (#29075) 2025-04-18 22:14:43 -03:00
Marshall Bowers
9875521d4e language_models: Fix passing of thread_id and prompt_id (#29071)
This PR is a follow-up to
https://github.com/zed-industries/zed/pull/29069 that fixes an issue
where the thread ID and prompt ID were not being sent up correctly.

Release Notes:

- N/A
2025-04-18 21:12:23 +00:00
Michael Sloan
8c55063417 Fix zed sometimes stopping by using setsid on interactive shells (#29070)
For some reason `SIGTTIN` sometimes gets sent to the process group,
causing it to stop when run from a terminal. This solves that issue by
putting the shell in a new session + progress group.

This allows removal of a workaround of using `exit 0;` to restore
handling of ctrl-c after exit. In testing this appears to no longer be
necessary.

Closes #27716

Release Notes:

- Fixed Zed sometimes becoming a stopped background process when run
from a terminal.
2025-04-18 15:04:26 -06:00
Marshall Bowers
7abe2c9c31 agent: Attach thread ID and prompt ID to telemetry events (#29069)
This PR attaches the thread ID and the new prompt ID to telemetry events
for completions in the Agent panel.

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-04-18 20:41:02 +00:00
Michael Sloan
73a767fc45 Add hidden prompt_to_focus field to OpenPromptLibrary action (#29062)
Release Notes:

- N/A
2025-04-18 20:39:40 +00:00
Michael Sloan
327fee4d22 Init prompt store in agent eval (#29068)
Needed after #28915

Release Notes:

- N/A
2025-04-18 20:06:34 +00:00
Joseph T. Lyons
b1d5918fdc Fix reset_db script (#29067)
Release Notes:

- N/A
2025-04-18 19:28:14 +00:00
renovate[bot]
6f685b9f8e Update Rust crate sea-orm to v1.1.10 (#28918)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [sea-orm](https://www.sea-ql.org/SeaORM)
([source](https://redirect.github.com/SeaQL/sea-orm)) | dev-dependencies
| patch | `1.1.8` -> `1.1.10` |
| [sea-orm](https://www.sea-ql.org/SeaORM)
([source](https://redirect.github.com/SeaQL/sea-orm)) | dependencies |
patch | `1.1.8` -> `1.1.10` |

---

### Release Notes

<details>
<summary>SeaQL/sea-orm (sea-orm)</summary>

###
[`v1.1.10`](https://redirect.github.com/SeaQL/sea-orm/blob/HEAD/CHANGELOG.md#1110---2025-04-14)

[Compare
Source](https://redirect.github.com/SeaQL/sea-orm/compare/1.1.9...1.1.10)

##### Upgrades

- Upgrade sqlx to 0.8.4
[https://github.com/SeaQL/sea-orm/pull/2562](https://redirect.github.com/SeaQL/sea-orm/pull/2562)

###
[`v1.1.9`](https://redirect.github.com/SeaQL/sea-orm/blob/HEAD/CHANGELOG.md#119---2025-04-14)

[Compare
Source](https://redirect.github.com/SeaQL/sea-orm/compare/1.1.8...1.1.9)

##### Enhancements

- \[sea-orm-macros] Use fully-qualified syntax for ActiveEnum associated
type[https://github.com/SeaQL/sea-orm/pull/2552](https://redirect.github.com/SeaQL/sea-orm/pull/2552)2
- Accept `LikeExpr` in `like` and `not_like`
[https://github.com/SeaQL/sea-orm/pull/2549](https://redirect.github.com/SeaQL/sea-orm/pull/2549)

##### Bug fixes

- Check if url is well-formed before parsing
[https://github.com/SeaQL/sea-orm/pull/2558](https://redirect.github.com/SeaQL/sea-orm/pull/2558)
- `QuerySelect::column_as` method cast ActiveEnum column
[https://github.com/SeaQL/sea-orm/pull/2551](https://redirect.github.com/SeaQL/sea-orm/pull/2551)

##### House keeping

- Remove redundant `Expr::expr` from internal code
[https://github.com/SeaQL/sea-orm/pull/2554](https://redirect.github.com/SeaQL/sea-orm/pull/2554)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjI0OC40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-04-18 19:15:02 +00:00
Smit Barmase
65a7076ba8 buffer_search: Fix DeployReplace not working when buffer search is already deployed (#29066)
Closes #29000

When buffer search is already deployed:
1. If find dialog is enabled, change it to find-and-replace dialog and
focuses to it
2. If find-and-replace is enabled, focuses to it 

Release Notes:

- Fixed an issue where invoking `DeployReplace` while the Find dialog
was open did not switch to the Find & Replace dialog.
- Fixed an issue where invoking `DeployReplace` while the Find & Replace
dialog was already open did not focus it.
2025-04-19 00:26:02 +05:30
Marshall Bowers
3fe15ee915 renovate: Require dependency dashboard approval for updates (#29065)
This PR changes Renovate's behavior to require dependency dashboard
approval for updates.

> Maybe you find Renovate too noisy, and want to opt-out of getting
automatic updates whenever they're ready.
>
> In this case, you can tell Renovate to wait for your approval before
making any pull requests. This means that you have full control over
when you get updates.
>
> But vulnerability remediation PRs will still get created immediately
without requiring approval.
>
> To require manual approval for *all* updates, add the
`:dependencyDashboardApproval` presets to the `extends` array in your
Renovate configuration file:
>
> —
https://docs.renovatebot.com/key-concepts/dashboard/#require-approval-for-all-updates

Release Notes:

- N/A
2025-04-18 18:44:30 +00:00
renovate[bot]
bae3ef01c6 Update Rust crate clap to v4.5.36 (#28905)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://redirect.github.com/clap-rs/clap) |
workspace.dependencies | patch | `4.5.35` -> `4.5.36` |

---

### Release Notes

<details>
<summary>clap-rs/clap (clap)</summary>

###
[`v4.5.36`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4536---2025-04-11)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.35...v4.5.36)

##### Fixes

- *(help)* Revert 4.5.35's "Don't leave space for shorts if there are
none" for now

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjI0OC40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-04-18 18:11:29 +00:00
renovate[bot]
810b39ce34 Update Rust crate aws-sdk-bedrockruntime to v1.82.0 (#28451)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[aws-sdk-bedrockruntime](https://redirect.github.com/awslabs/aws-sdk-rust)
| workspace.dependencies | minor | `1.80.0` -> `1.82.0` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-04-18 17:42:51 +00:00
Michael Sloan
a93aa598d6 Fix documentation of impl_action_with_deprecated_aliases (#29063)
Release Notes:

- N/A
2025-04-18 17:35:48 +00:00
renovate[bot]
28aba94369 Update actions/setup-node digest to 49933ea (#28897)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-node](https://redirect.github.com/actions/setup-node) |
action | digest | `cdca736` -> `49933ea` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-18 11:31:02 -06:00
renovate[bot]
e147ef006c Update Rust crate libc to v0.2.172 (#28911)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [libc](https://redirect.github.com/rust-lang/libc) |
workspace.dependencies | patch | `0.2.171` -> `0.2.172` |

---

### Release Notes

<details>
<summary>rust-lang/libc (libc)</summary>

###
[`v0.2.172`](https://redirect.github.com/rust-lang/libc/releases/tag/0.2.172)

[Compare
Source](https://redirect.github.com/rust-lang/libc/compare/0.2.171...0.2.172)

##### Added

- Android: Add `getauxval` for 32-bit targets
([#&#8203;4338](https://redirect.github.com/rust-lang/libc/pull/4338))
- Android: Add `if_tun.h` ioctls
([#&#8203;4379](https://redirect.github.com/rust-lang/libc/pull/4379))
- Android: Define `SO_BINDTOIFINDEX`
([#&#8203;4391](https://redirect.github.com/rust-lang/libc/pull/4391))
- Cygwin: Add `posix_spawn_file_actions_add[f]chdir[_np]`
([#&#8203;4387](https://redirect.github.com/rust-lang/libc/pull/4387))
- Cygwin: Add new socket options
([#&#8203;4350](https://redirect.github.com/rust-lang/libc/pull/4350))
- Cygwin: Add statfs & fcntl
([#&#8203;4321](https://redirect.github.com/rust-lang/libc/pull/4321))
- FreeBSD: Add `filedesc` and `fdescenttbl`
([#&#8203;4327](https://redirect.github.com/rust-lang/libc/pull/4327))
- Glibc: Add unstable support for \_FILE_OFFSET_BITS=64
([#&#8203;4345](https://redirect.github.com/rust-lang/libc/pull/4345))
- Hermit: Add `AF_UNSPEC`
([#&#8203;4344](https://redirect.github.com/rust-lang/libc/pull/4344))
- Hermit: Add `AF_VSOCK`
([#&#8203;4344](https://redirect.github.com/rust-lang/libc/pull/4344))
- Illumos, NetBSD: Add `timerfd` APIs
([#&#8203;4333](https://redirect.github.com/rust-lang/libc/pull/4333))
- Linux: Add `_IO`, `_IOW`, `_IOR`, `_IOWR` to the exported API
([#&#8203;4325](https://redirect.github.com/rust-lang/libc/pull/4325))
- Linux: Add `tcp_info` to uClibc bindings
([#&#8203;4347](https://redirect.github.com/rust-lang/libc/pull/4347))
- Linux: Add further BPF program flags
([#&#8203;4356](https://redirect.github.com/rust-lang/libc/pull/4356))
- Linux: Add missing INPUT_PROP_XXX flags from `input-event-codes.h`
([#&#8203;4326](https://redirect.github.com/rust-lang/libc/pull/4326))
- Linux: Add missing TLS bindings
([#&#8203;4296](https://redirect.github.com/rust-lang/libc/pull/4296))
- Linux: Add more constants from `seccomp.h`
([#&#8203;4330](https://redirect.github.com/rust-lang/libc/pull/4330))
- Linux: Add more glibc `ptrace_sud_config` and related
`PTRACE_*ET_SYSCALL_USER_DISPATCH_CONFIG`.
([#&#8203;4386](https://redirect.github.com/rust-lang/libc/pull/4386))
- Linux: Add new netlink flags
([#&#8203;4288](https://redirect.github.com/rust-lang/libc/pull/4288))
- Linux: Define ioctl codes on more architectures
([#&#8203;4382](https://redirect.github.com/rust-lang/libc/pull/4382))
- Linux: Add missing `pthread_attr_setstack`
([#&#8203;4349](https://redirect.github.com/rust-lang/libc/pull/4349))
- Musl: Add missing `utmpx` API
([#&#8203;4332](https://redirect.github.com/rust-lang/libc/pull/4332))
- Musl: Enable `getrandom` on all platforms
([#&#8203;4346](https://redirect.github.com/rust-lang/libc/pull/4346))
- NuttX: Add more signal constants
([#&#8203;4353](https://redirect.github.com/rust-lang/libc/pull/4353))
- QNX: Add QNX 7.1-iosock and 8.0 to list of additional cfgs
([#&#8203;4169](https://redirect.github.com/rust-lang/libc/pull/4169))
- QNX: Add support for alternative Neutrino network stack `io-sock`
([#&#8203;4169](https://redirect.github.com/rust-lang/libc/pull/4169))
- Redox: Add more `sys/socket.h` and `sys/uio.h` definitions
([#&#8203;4388](https://redirect.github.com/rust-lang/libc/pull/4388))
- Solaris: Temporarily define `O_DIRECT` and `SIGINFO`
([#&#8203;4348](https://redirect.github.com/rust-lang/libc/pull/4348))
- Solarish: Add `secure_getenv`
([#&#8203;4342](https://redirect.github.com/rust-lang/libc/pull/4342))
- VxWorks: Add missing `d_type` member to `dirent`
([#&#8203;4352](https://redirect.github.com/rust-lang/libc/pull/4352))
- VxWorks: Add missing signal-related constsants
([#&#8203;4352](https://redirect.github.com/rust-lang/libc/pull/4352))
- VxWorks: Add more error codes
([#&#8203;4337](https://redirect.github.com/rust-lang/libc/pull/4337))

##### Deprecated

- FreeBSD: Deprecate `TCP_PCAP_OUT` and `TCP_PCAP_IN`
([#&#8203;4381](https://redirect.github.com/rust-lang/libc/pull/4381))

##### Fixed

- Cygwin: Fix member types of `statfs`
([#&#8203;4324](https://redirect.github.com/rust-lang/libc/pull/4324))
- Cygwin: Fix tests
([#&#8203;4357](https://redirect.github.com/rust-lang/libc/pull/4357))
- Hermit: Make `AF_INET = 3`
([#&#8203;4344](https://redirect.github.com/rust-lang/libc/pull/4344))
- Musl: Fix the syscall table on RISC-V-32
([#&#8203;4335](https://redirect.github.com/rust-lang/libc/pull/4335))
- Musl: Fix the value of `SA_ONSTACK` on RISC-V-32
([#&#8203;4335](https://redirect.github.com/rust-lang/libc/pull/4335))
- VxWorks: Fix a typo in the `waitpid` parameter name
([#&#8203;4334](https://redirect.github.com/rust-lang/libc/pull/4334))

##### Removed

- Musl: Remove `O_FSYNC` on RISC-V-32 (use `O_SYNC` instead)
([#&#8203;4335](https://redirect.github.com/rust-lang/libc/pull/4335))
- Musl: Remove `RTLD_DEEPBIND` on RISC-V-32
([#&#8203;4335](https://redirect.github.com/rust-lang/libc/pull/4335))

##### Other

- CI: Add matrix env variables to the environment
([#&#8203;4345](https://redirect.github.com/rust-lang/libc/pull/4345))
- CI: Always deny warnings
([#&#8203;4363](https://redirect.github.com/rust-lang/libc/pull/4363))
- CI: Always upload successfully created artifacts
([#&#8203;4345](https://redirect.github.com/rust-lang/libc/pull/4345))
- CI: Install musl from source for loongarch64
([#&#8203;4320](https://redirect.github.com/rust-lang/libc/pull/4320))
- CI: Revert "Also skip `MFD_EXEC` and `MFD_NOEXEC_SEAL` on sparc64"
([#]())
- CI: Use `$PWD` instead of `$(pwd)` in run-docker
([#&#8203;4345](https://redirect.github.com/rust-lang/libc/pull/4345))
- Solarish: Restrict `openpty` and `forkpty` polyfills to Illumos,
replace Solaris implementation with bindings
([#&#8203;4329](https://redirect.github.com/rust-lang/libc/pull/4329))
- Testing: Ensure the makedev test does not emit unused errors
([#&#8203;4363](https://redirect.github.com/rust-lang/libc/pull/4363))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-18 11:30:40 -06:00
renovate[bot]
e18d787dbc Update Rust crate proc-macro2 to v1.0.95 (#28917)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [proc-macro2](https://redirect.github.com/dtolnay/proc-macro2) |
workspace.dependencies | patch | `1.0.94` -> `1.0.95` |

---

### Release Notes

<details>
<summary>dtolnay/proc-macro2 (proc-macro2)</summary>

###
[`v1.0.95`](https://redirect.github.com/dtolnay/proc-macro2/releases/tag/1.0.95)

[Compare
Source](https://redirect.github.com/dtolnay/proc-macro2/compare/1.0.94...1.0.95)

- Update semver-exempt API under
`RUSTFLAGS=--cfg=procmacro2_semver_exempt` to that of nightly-2025-04-16
([#&#8203;497](https://redirect.github.com/dtolnay/proc-macro2/issues/497))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-18 11:30:22 -06:00
renovate[bot]
d02c8bcc02 Update Rust crate mimalloc to v0.1.46 (#28912)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mimalloc](https://redirect.github.com/purpleprotocol/mimalloc_rust) |
dependencies | patch | `0.1.45` -> `0.1.46` |

---

### Release Notes

<details>
<summary>purpleprotocol/mimalloc_rust (mimalloc)</summary>

###
[`v0.1.46`](https://redirect.github.com/purpleprotocol/mimalloc_rust/releases/tag/v0.1.46):
Version 0.1.46

[Compare
Source](https://redirect.github.com/purpleprotocol/mimalloc_rust/compare/v0.1.45...v0.1.46)

##### Changes

-   Fixed musl builds.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-18 11:30:07 -06:00
Danilo Leal
e27f6a984f agent: Simplify design of the settings view (#29041)
Containing everything in boxes wasn't super necessary here. Want to
still improve the switch color contrast here, but will probably do that
in a separate PR.

<img
src="https://github.com/user-attachments/assets/f826a7a8-beaf-45d0-9dc2-36dc210c418e"
width="700"/>

Release Notes:

- N/A
2025-04-18 14:24:53 -03:00
Joseph T. Lyons
cce661b64b docs: Add documentation about signing in to Zed (#29054)
Release Notes:

- N/A
2025-04-18 17:09:23 +00:00
Smit Barmase
3932a6c51e pane: Fix double or invisible borders in tab bar (#29061)
Invisible borders:
<img width="349" alt="Screenshot 2025-04-18 at 3 59 03 PM"
src="https://github.com/user-attachments/assets/a3a43885-ce87-4fcf-864a-d730fea1551e"
/>
<img width="547" alt="Screenshot 2025-04-18 at 8 23 15 PM"
src="https://github.com/user-attachments/assets/1f8669a8-f893-4c58-ba30-025be1bc733f"
/>

Double borders:
<img width="295" alt="Screenshot 2025-04-18 at 3 56 48 PM"
src="https://github.com/user-attachments/assets/7b4ae42d-c7fd-478c-97ce-10abefe4a482"
/>

Release Notes:

- N/A
2025-04-18 22:32:22 +05:30
tidely
1e0ae35f69 gpui: Make MacPlatform::os_version infallible (#29008)
Core change:
```rust
fn os_version() -> Result<SemanticVersion>
```

```rust
fn os_version() -> SemanticVersion
```


Release Notes:

- N/A
2025-04-18 11:00:43 -06:00
Oleksiy Syvokon
4405ed04d0 linux: Fix cursor-related panic on Wayland (#29060)
This fixes the panic that happened in debug builds in Wayland when
focusing/defocusing window in the edit mode:

```
"Thread "main" panicked with "CursorStyle::None should be handled separately in the client" at crates/gpui/src/platform/linux/wayland.rs:40:17"
```

Full log:
[stacktrace.txt](https://github.com/user-attachments/files/19814411/stacktrace.txt)

@smitbarmase, you seem to have worked on this code. Tagging you for
visibility :)

Release Notes:

- N/A
2025-04-18 17:00:19 +00:00
Smit Barmase
c585dbd8ff git_panel: Fix amend check (#29059)
`is_some` -> `is_none` 

Release Notes:

- N/A
2025-04-18 22:24:49 +05:30
Michael Sloan
c7fc95e732 Remove .direnv from .gitignore as the correct file is .envrc (#29058)
Release Notes:

- N/A
2025-04-18 16:53:41 +00:00
chbk
f97546b6ef Improve Regex highlighting (#28183)
| Zed 0.180.2 | With this PR |
| --- | --- |
|
![Image](https://github.com/user-attachments/assets/e840bd81-25ff-4c7a-af03-bac6db11f910)
|
![Image](https://github.com/user-attachments/assets/3fd58164-8992-44e1-be01-8c6d70f9587d)
|

```js
match = "424242"
regex = /(42)+?\d{2}\1/g
```

- `/`: `operator` -> `string.regex` (like `"` for regex strings)
- `+?`: `operator.regex`
- `\d`: `string.escape` -> `string.escape.regex`
- `\1`: `keyword.operator.regex` (backreference)
- `/g`: `keyword.regex` -> `keyword.operator.regex`
- `{2}`: `number` -> `number.quantifier.regex`

Release Notes:

  - Improved Regex highlighting
2025-04-18 12:44:13 -04:00
Kirill Bulatov
7badd6053d debugger: Fix gutter tasks display for users without the debugger feature flag (#29056) 2025-04-18 10:22:26 -06:00
Michael Sloan
502a0f6535 agent: Use default prompts from prompt library in system prompt (#28915)
Related to #28490.

- Default prompts from the prompt library are now included as "user
rules" in the system prompt.
- Presence of these user rules is shown at the beginning of the thread
in the UI.
_ Now uses an `Entity<PromptStore>` instead of an `Arc<PromptStore>`.
Motivation for this is emitting a `PromptsUpdatedEvent`.
- Now disallows concurrent reloading of the system prompt. Before this
change it was possible for reloads to race.

Release Notes:

- agent: Added support for including default prompts from the Prompt
Library as "user rules" in the system prompt.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-04-18 09:32:35 -06:00
Marshall Bowers
eea6cfb383 collab: Upgrade from Zed Pro trial to Zed Pro by ending trial period early (#29052)
This PR adjusts the upgrade from a Zed Pro trial to Zed Pro to do so by
ending the trial period early.

This will transition the subscription to `active` and bill the user
without needing to send them through a Stripe Checkout flow.

Release Notes:

- N/A
2025-04-18 15:29:22 +00:00
Marshall Bowers
0dc0701967 Show edit predictions usage in status bar menu (#29046)
This PR adds an indicator for edit predictions usage in the edit
predictions menu:

| Free | Zed Pro / Trial |
|
---------------------------------------------------------------------------------------------------------------------------------------------------
|
---------------------------------------------------------------------------------------------------------------------------------------------------
|
| <img width="235" alt="Screenshot 2025-04-18 at 9 53 47 AM"
src="https://github.com/user-attachments/assets/6da001d2-ef9c-49df-86be-03d4c615d45c"
/> | <img width="237" alt="Screenshot 2025-04-18 at 9 54 33 AM"
src="https://github.com/user-attachments/assets/31f5df04-a8e1-43ec-8af7-ebe501516abe"
/> |

Only visible to users on the new billing.

Release Notes:

- N/A
2025-04-18 14:15:19 +00:00
moaqz
62b8ef980b docs: Fix broken links (#29042)
This PR fixes some broken links in the docs.

Release Notes:

- N/A
2025-04-18 09:38:40 -04:00
Marshall Bowers
269f6403dd snippet_provider: Use proper casing of VsCode in identifiers (#29038)
This PR renames some identifiers in the `snippet_provider` to use the
correct casing of `VsCode`.

Release Notes:

- N/A
2025-04-18 12:11:54 +00:00
Bennet Bo Fenner
3538acec7c agent: Do not insert selection as context when selection is empty (#29031)
Release Notes:

- N/A
2025-04-18 09:31:26 +00:00
Bennet Bo Fenner
87512d0814 agent: Remove selections as context once message is sent (#29030)
Release Notes:

- N/A
2025-04-18 08:50:46 +00:00
5brian
6254efe39d vim: Fix character count in visual line mode (#28669)
Closes https://github.com/zed-industries/zed/issues/10727

Release Notes:

- vim: Fixed character count in visual line mode

Co-authored-by: Conrad Irwin <conrad@zed.dev>
2025-04-17 22:14:44 -06:00
redforks
72218f4a61 Make Copy and Trim ignore empty lines, and fix vim line selections (#29019)
Close #28519 

Release Notes:

Update `editor: copy and trim` command:

1. Ignore empty lines in the middle:

    ```
      Line 1

      Line 2
    ```

    Will copy text to clipboard:

    ```
    Line 1

    Line 2
    ```

    Before this commit trim not performed

1. Fix select use vim line selections, trim not works
2025-04-17 21:35:05 -06:00
AidanV
5f7189e5af vim: Change line up and change line down respect indentation (#28934)
When using 'c' with line-wise motions like j/k, operate like cc to fix
indentation issues.

Closes #28933 

Release Notes:

- `c j` and `c k` now respect indentation
2025-04-17 20:51:24 -06:00
Conrad Irwin
f6d13645ce Fix error logging (#29010)
Co-Authored-By: Ben <ben@zed.dev>

Release Notes:

- N/A

Co-authored-by: Ben <ben@zed.dev>
2025-04-17 20:58:54 -04:00
João Marcos
6ffd3f034f Don't display MacOS key symbols in Linux (#29016)
Release Notes:

- Fix MacOS key symbols being displayed in other platforms.
2025-04-17 21:48:00 -03:00
Smit Barmase
6e0732a9d7 git_ui: Fix amend not working for detached HEAD (#29017)
Closes #28736

Release Notes:

- Fixed git amend not working for detached HEAD.
2025-04-18 06:15:54 +05:30
Michael Sloan
f8d097acd6 Initial .rules file for agent with symlinks for other rules file paths (#29014)
Release Notes:

- N/A
2025-04-17 23:41:23 +00:00
Michael Sloan
7cf4926130 Misc GPUI Entity<T> cleanups (#28996)
Found these while working on a `.rules` file which explains how GPUI
works.

Release Notes:

- N/A
2025-04-17 23:29:19 +00:00
Marshall Bowers
676cc109a3 agent: Report usage from thread summarization requests (#29012)
This PR makes it so the thread summarization also reports the model
request usage, to prevent the case where the count would appear to jump
by 2 the next time a message was sent after summarization.

Release Notes:

- N/A
2025-04-17 23:05:12 +00:00
Smit Barmase
ba7f886c62 project: Show detached head commit SHA in branch pickers (#29007)
When Git is in a detached HEAD state, the branch is `None`, and we can't
get any meaningful information from it. This PR adds a `head_commit`
field to the snapshot, which is always populated with the HEAD details,
even when the branch is `None`.

This also pave path to fix:
https://github.com/zed-industries/zed/issues/28736

git panel branch picker (before, after):
<img width="197" alt="image"
src="https://github.com/user-attachments/assets/0b6abbba-2988-4890-a708-bcd8aad84f26"
/> <img width="198" alt="image"
src="https://github.com/user-attachments/assets/4b08b1a8-5e79-4aa3-a44e-932249602c18"
/>

title bar branch picker (before, after):
<img width="183" alt="image"
src="https://github.com/user-attachments/assets/d94357f8-a4da-4d60-8ddd-fdd978b99fdf"
/> <img width="228" alt="image"
src="https://github.com/user-attachments/assets/d20824a1-9279-44d6-afd1-bf9319fc50e4"
/>

Release Notes:

- Added head commit SHA information to the Git branch picker in the
title bar and Git panel.
2025-04-18 04:23:56 +05:30
Marshall Bowers
c2cd4fd7a1 agent: Show request usage in the panel (#29006)
This PR adds a banner showing request usage in the Agent panel:

<img width="640" alt="Screenshot 2025-04-17 at 5 51 46 PM"
src="https://github.com/user-attachments/assets/e0eb036c-57c1-441c-bbab-7dab1c6e56d9"
/>

Only visible to users on the new billing.

Note to Joseph: Doesn't need to be cherry-picked to Preview.

Release Notes:

- N/A

---------

Co-authored-by: Nate <nate@zed.dev>
2025-04-17 22:16:57 +00:00
Cole Miller
4095011af5 debugger_ui: Show a toast when setting breakpoints fails (#28815)
Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
2025-04-17 22:10:57 +00:00
5brian
80a2f71d8e vim: Add ctrl-^ (#28648)
Alias for Ctrl-6: https://neovim.io/doc/user/editing.html#CTRL-%5E

Also removed Ctrl-6 from the ProjectPanel context, iiuc, it shouldn't
have any effect there

Release Notes:

- vim: Added `ctrl-^` as an alias for `ctrl-6` in the default vim keymap
2025-04-17 17:54:18 -04:00
Marshall Bowers
d93141bded agent: Extract usage information from response headers (#29002)
This PR updates the Agent to extract the usage information from the
response headers, if they are present.

For now we just log the information, but we'll be using this soon to
populate some UI.

Release Notes:

- N/A
2025-04-17 20:11:07 +00:00
AidanV
b402007de6 nix: Add libX11 dependency for X11 support (#28938)
Closes #28937 

Release Notes:

- N/A
2025-04-17 12:58:45 -07:00
Marshall Bowers
be63d51eb7 zeta: Extract usage information from response headers (#28999)
This PR updates the Zeta provider to extract the usage information from
the response headers, if they are present.

For now we just log the information, but we'll need to figure out where
this needs to get threaded through to in order to display it in the UI.

Release Notes:

- N/A
2025-04-17 19:07:40 +00:00
Anthony Eid
8660101b83 debugger: Configure default pane layout conditionally based on capabilities (#28991)
This fixes a debug panic that happened when closing a debug session item
through the debug panel context menu. The default layout now only
includes module list and loaded sources list if they're supported.


Release Notes:

- N/A
2025-04-17 14:46:50 -04:00
João Marcos
1aa1b2bede Fix multiline completions when surroundings don't match completion text (#28995)
Follow up to the scenarios I overlooked in
https://github.com/zed-industries/zed/pull/28586.

Release Notes:

- N/A
2025-04-17 15:37:38 -03:00
Marshall Bowers
58d8b91131 collab: Treat trialing subscriptions as active (#28992)
This PR makes it so billing subscriptions in the `trialing` state are
considered `active`.

Release Notes:

- N/A
2025-04-17 18:19:34 +00:00
Smit Barmase
ba588161d9 editor: Revert flattening of code actions in mouse context menu (#28988)
In light of making context not move dynamically, reverting back these
changes.

- Doing it async will lead to a loading state, which moves the context
menu.
- Doing it sync introduces noticeable lag in opening the context menu.
   
Future idea is to introduce fixed code actions like refactor, rewrite,
etc depending on code action kind [(see
more)](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionKind)
which will use submenus.
 
Release Notes:

- N/A
2025-04-17 23:48:51 +05:30
Max Brunsfeld
7e928dd615 Implement dragging external files to remote projects (#28987)
Release Notes:

- Added the ability to copy external files into remote projects by
dragging them onto the project panel.

---------

Co-authored-by: Peter Tripp <petertripp@gmail.com>
2025-04-17 11:06:56 -07:00
Marshall Bowers
fade49a11a collab: Don't use a separate product for Zed Pro trials (#28986)
This PR removes the separate product used for the Zed Pro trials, in
favor of using Stripe's trial functionality.

Release Notes:

- N/A
2025-04-17 17:44:14 +00:00
chbk
e4f692ac75 html: Improve syntax highlighting (#28184)
| Zed 0.180.2 | With this PR |
| --- | --- |
|
![Image](https://github.com/user-attachments/assets/89d70ba1-791b-462e-9a14-31c75bcebb7e)
|
![Image](https://github.com/user-attachments/assets/9199499e-071e-49b3-8536-b04b8ce5a222)
|


```html
<script>
  return <div class="main content"></div>
</script>
<div class="main content"></div>
<span></spn>
```

Changes homogenize JSX and HTML

- `"`: `string`
- `=`: `operator` -> `punctuation.delimiter` like in
[JSX](3775496b84/crates/languages/src/javascript/highlights.scm (L246)),
[VSCode](336801752d/extensions/html/syntaxes/html.tmLanguage.json (L382))
- `erroneous_end_tag_name`: `keyword` -> not a keyword

Release Notes:

  - Improved HTML highlighting
2025-04-17 13:40:56 -04:00
Noah Lemen
c21bca07e2 Correct typos in GPUI key_dispatch.rs comments (#28926)
just noticed an extra semicolon and a reference to the nonexistant
`keymap_context` function!

Release Notes:

- N/A
2025-04-17 13:33:01 -04:00
Nate Butler
acc4a5ccb3 Add example agent tool preview (#28984)
This PR adds an example of rendering previews for tools using the new
Agent ToolCard style.

![CleanShot 2025-04-17 at 13 03
12@2x](https://github.com/user-attachments/assets/d4c7d266-cc32-4038-9170-f3e070fce60e)

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-04-17 17:29:19 +00:00
Conrad Irwin
7a95c14625 Revert "git_panel: Pad end of list to avoid obscuring final entry with horizontal scrollbar (#28823)" (#28971)
This reverts commit 1d98b33ae0.

Not sure why, but seems like this breaks the binary search used to
correlate items to each other in the lists.

Release Notes:

- N/A
2025-04-17 11:15:12 -06:00
Oleksiy Syvokon
6dd622d6c3 eval: Fix git revision existence check (#28959)
This change fixes a bug in the worktree initialization.

Details: `git ref-parse --verify $HASH` just checks that $HASH is a
well-formed hash and will successfully return even if $HASH doesn't
exist.

Release Notes:

- N/A
2025-04-17 19:57:37 +03:00
Ben Kunkle
e7afbbd725 editor: Dismiss mouse context menus on selections change (#28729)
Closes #ISSUE

Adds an extra subscription for mouse context menus (i.e. right click context menu) so that when selections change in the editor while the context menu is open (e.g. with vim motions), the context menu closes.

Release Notes:

- N/A
2025-04-17 12:38:12 -04:00
Mikayla Maki
133932ed74 Add support for remote branches to the branch picker (#28978)
Release Notes:

- Added support for remote branches to the branch picker

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-04-17 16:13:02 +00:00
Kirill Bulatov
3ca63584b9 Escape all runnables' cargo extra arguments coming from rust-analyzer (#28977)
Closes https://github.com/zed-industries/zed/issues/28947

Release Notes:

- Fixed certain doctests not being run properly
2025-04-17 16:05:30 +00:00
Danilo Leal
2a878ee6d0 agent: Add design tweaks (#28963)
One more batch of fine-tuning the agent panel's design.

Release Notes:

- N/A
2025-04-17 12:20:25 -03:00
Umesh Yadav
8117940aca Add support for OpenAI o3 and o4-mini models (#28881)
Release Notes:

- Add support for OpenAI o3 and o4-mini models via OpenAI API and
Copilot Chat providers.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-04-17 10:58:41 -04:00
Bennet Bo Fenner
002235d0da agent: Support adding selection as context (#28964)
https://github.com/user-attachments/assets/42ebe911-3392-48f7-8583-caab285aca09

Release Notes:

- agent: Support adding selections via @selection or `assistant: Quote
selection` as context
2025-04-17 16:55:15 +02:00
Agus Zubiaga
f07695c4cd Remove evals crate (#28968)
Release Notes: 
- N/A
2025-04-17 14:53:22 +00:00
redforks
bdd0cbb717 Fix snippets from extensions being listed twice (#28940)
lookup_snippets() merges global snippets and extension snippets, but
global_snippets::lookup_snippets() also returns extension snippets, make
them double

Closes #28661 

Release Notes:

- Fixed a bug where extension provided snippets were being displayed in
duplicate.
2025-04-17 10:43:49 -04:00
Danilo Leal
022a110f8e agent: Fix "open thread as markdown" button (#28962)
Just now realized that the reason this button wasn't working reliably is
because we weren't passing the index to it. It's now fixed.

Release Notes:

- N/A
2025-04-17 10:20:24 -03:00
Bennet Bo Fenner
b0200c4368 agent: Show context server name in incompatible tool warning (#28954)
<img width="410" alt="image"
src="https://github.com/user-attachments/assets/e29a0ba8-3d37-4e66-b90c-398b24da0453"
/>


Release Notes:

- N/A
2025-04-17 10:18:03 +00:00
Bennet Bo Fenner
ae47829fa8 agent: Fix system instructions typo (#28949)
See #28793, the name of the field is actually `systemInstruction` not
`systemInstructions`.

Release Notes:

- Fixed an issue where Gemini requests would fail
2025-04-17 08:51:05 +00:00
Smit Barmase
5ebb18c47e editor: Fix scrolling drag interrupted on gutter hovering (#28924)
Closes #27188

This PR fixes the issue where, when you drag the scroll handle of the
editor and your mouse hovers over the gutter of the next editor,
scrolling stops. I found no good reason to stop propagation on gutter
hover.

Release Notes:

- Fixed an issue where editor scrolling would stop when the mouse
hovered over another editor's gutter.
2025-04-17 11:48:33 +05:30
Conrad Irwin
ded1c7012c Set diagnostic width based on ems (#28936)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-04-16 22:31:04 -06:00
Conrad Irwin
ad25cd09b6 Fix panic when diagnostics first opens (#28935)
Closes #ISSUE

Release Notes:

- N/A
2025-04-16 21:57:42 -06:00
Agus Zubiaga
a7a7335da4 edit prediction: Assign providers when client status changes (#28919)
There was recently a change that caused the Zed Edit Prediction provider
to only be assigned when the client was connected. However, this check
happened too early, resulting in restored buffers never getting
registered. We'll now subscribe to client status changes and reassign
providers accordingly.

Release Notes:

- edit prediction: Fixed bug disabling prediction in restored buffers
2025-04-16 21:09:25 -03:00
Ben Kunkle
cbb6c221b3 Remove netcat dependency (#28920)
Closes #28813
Closes #27749

Release Notes:

- Removed the need to have openbsd `netcat` (`nc`) installed on your
system in order to enter passwords for `git` or `ssh` (remote
development). If you previously installed `netcat` specifically for Zed,
you may uninstall it.
2025-04-16 19:46:36 -04:00
Ben Kunkle
63b4b60b79 zlog: Ensure log file is flushed (#28923)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-04-16 23:30:23 +00:00
Conrad Irwin
9ea8a9a1d3 Fix more inlay/excerpt race conditions (#28914)
Closes #ISSUE

Release Notes:

- N/A
2025-04-16 16:18:02 -06:00
Anthony Eid
19f542b8d6 debugger: Clear dap status indicator when dap update/download is complete (#28913)
Release Notes:

- N/A
2025-04-16 21:22:08 +00:00
renovate[bot]
70b3cb04bb Update Rust crate anyhow to v1.0.98 (#28904)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [anyhow](https://redirect.github.com/dtolnay/anyhow) |
workspace.dependencies | patch | `1.0.97` -> `1.0.98` |

---

### Release Notes

<details>
<summary>dtolnay/anyhow (anyhow)</summary>

###
[`v1.0.98`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.98)

[Compare
Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.97...1.0.98)

- Add
[`self.into_boxed_dyn_error()`](https://docs.rs/anyhow/1/anyhow/struct.Error.html#method.into_boxed_dyn_error)
and
[`self.reallocate_into_boxed_dyn_error_without_backtrace()`](https://docs.rs/anyhow/1/anyhow/struct.Error.html#method.reallocate_into_boxed_dyn_error_without_backtrace)
methods for anyhow::Error
([#&#8203;415](https://redirect.github.com/dtolnay/anyhow/issues/415))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-16 17:04:22 -04:00
renovate[bot]
0170f522bf Pin actions/checkout action to 11bd719 (#28896)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://redirect.github.com/actions/checkout) |
action | pinDigest | -> `11bd719` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-16 17:03:49 -04:00
Kirill Bulatov
602ae84542 Fix the buttons not working in key context view (#28910)
Release Notes:

- N/A
2025-04-16 21:02:37 +00:00
Marshall Bowers
3fef3cc392 Use more types/constants from zed_llm_client (#28909)
This PR makes it so we use more types and constants from the
`zed_llm_client` crate to avoid duplicating information.

Also updates the current usage endpoint to use limits derived from the
`Plan`.

Release Notes:

- N/A
2025-04-16 20:58:00 +00:00
Anthony Eid
78c856cb75 debugger: Enable manually restarting a session when a DAP server doesn't support restarting (#28908)
This PR also fixes the unexpected behavior of clicking restart when a
session is terminated and nothing happens.

And we fixed a small bug where `DebugClientAdapter.shutdown()` was never
called.

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-04-16 20:56:37 +00:00
Smit Barmase
21946691a4 docs: Use inline code for action (#28907)
Oops, typo.

Release Notes:

- N/A
2025-04-17 02:02:15 +05:30
Marshall Bowers
fcb1efdf21 rpc: Remove llm module in favor of zed_llm_client (#28900)
This PR removes the `llm` module of the `rpc` crate in favor of using
the types from the `zed_llm_client`.

Release Notes:

- N/A
2025-04-16 20:22:44 +00:00
Smit Barmase
54b46fdfaa docs: Add example for disabling default binding while keeping custom one active (#28906)
Release Notes:

- N/A
2025-04-17 01:44:42 +05:30
Smit Barmase
94cf1b0353 outline_panel: Rename outline_panel::Open to outline_panel::OpenSelectedEntry (#28890)
Closes #27171

The `outline_panel::Open` action seems to open the outline panel, but
instead, it moves the editor's cursor to the position of the selected
entry in the outline panel. This PR renames it to
`outline_panel::OpenSelectedEntry` for better clarity.

Meanwhile, there is an existing action, `outline_panel::ToggleFocus`,
that should be used for opening the outline panel.

Todo:
- [x] Added migration

Release Notes:

- Renamed `outline_panel::Open` to `outline_panel::OpenSelectedEntry`
for better clarity.
2025-04-17 01:44:00 +05:30
Kirill Bulatov
56856fb992 Add a way to navigate between changes (#28891)
Closes https://github.com/zed-industries/zed/issues/19731

Adds `editor::GoToPreviousChange` and `editor::GoToNextChange` that work
the same as `vim::ChangeListOlder` and `vim::ChangeListNewer` as the
common logic was extracted and reused.

Release Notes:

- Added a way to navigate between changes with
`editor::GoToPreviousChange` and `editor::GoToNextChange`
2025-04-16 14:09:17 -06:00
Conrad Irwin
64a67a1071 Remove DebugAdapterConfig (#28898)
This is unused as of recent changes to task spawning.

Release Notes:

- N/A
2025-04-16 14:02:10 -06:00
Conrad Irwin
040046ed2a Show all warnings (#28899)
Release Notes:

- (preview only) Fixes a bug where some warnings were not rendered
correctly in the Diagnostics view
2025-04-16 13:49:40 -06:00
Agus Zubiaga
0286b8ab3e agent: Fix conversation token usage and estimate unsent message (#28878)
The UI was mistakenly using the cumulative token usage for the token
counter. It will now display the last request token count, plus an
estimation of the tokens in the message editor and context entries that
haven't been sent yet.


https://github.com/user-attachments/assets/0438c501-b850-4397-9135-57214ca3c07a

Additionally, when the user edits a message, we'll display the actual
token count up to it and estimate the tokens in the new message.

Note: We don't currently estimate the delta when switching profiles. In
the future, we want to use the count tokens API to measure every part of
the request and display a breakdown.

Release Notes:

- agent: Made the token count more accurate and added back estimation of
used tokens as you type and add context.

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-04-16 16:27:36 -03:00
Thomas Mickley-Doyle
8de53bd89f agent: Add git commit ID to the eval telemetry data (#28895)
Release Notes:

- N/A
2025-04-16 14:13:43 -05:00
Marshall Bowers
10507f9a4c collab: Add plan column to subscription_usages (#28889)
This PR adds a `plan` column to the `subscription_usages` table.

These tables don't have any records in them yet, so it's fine to make
the column required without a default.

Release Notes:

- N/A
2025-04-16 18:36:49 +00:00
Conrad Irwin
7bdde8f14f Fix anchor_in_excerpt on replaced excerpts (#28880)
Release Notes:

- N/A
2025-04-16 12:35:40 -06:00
Bennet Bo Fenner
7c7f69f4c5 agent: Allow quoting selection when text thread is active (#28887)
This makes the `assistant: Quote selection` work again for text threads.
Next up is supporting this also in normal threads.

Release Notes:

- agent: Add support for inserting selections (assistant: Quote
selection) into text threads
2025-04-16 20:32:08 +02:00
Mikayla Maki
12c9526f6a Remove bottom dock layout button (#28876)
Release Notes:

- Preview: Removed the layout button from the title bar. The
`bottom_dock_layout` setting still functions.
- Added a setting, `bottom_dock_layout`, for controlling the
relationship between the bottom dock and the left and right docks.
2025-04-16 18:29:36 +00:00
Marshall Bowers
97b044acf5 proto: Add ZedProTrial to Plan (#28885)
This PR adds the `ZedProTrial` member to the `Plan` enum.

Release Notes:

- N/A
2025-04-16 18:13:00 +00:00
Bennet Bo Fenner
1e25e6b3cc agent: Improve fuzzy matching for @mentions (#28883)
Make fuzzy search in @-mention match paths and context kinds as well
(e.g., typing "sym" should let me select the "Symbols" label, as opposed
to just paths)

Release Notes:

- agent: Improve fuzzy-matching when using @mentions
2025-04-16 17:44:07 +00:00
Anthony Eid
f565994da9 debugger: Remove or move breakpoints on file deletion/rename (#28882)
Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-04-16 17:41:24 +00:00
Danilo Leal
db94d6d767 agent: Add item to open Prompt Library in the panel's menu (#28877)
Release Notes:

- agent: Added a menu item to open the Prompt Library from the panel's
dropdown menu on the top right.
2025-04-16 14:31:34 -03:00
Bennet Bo Fenner
456e54b87c agent: Add websearch tool (#28621)
Staff only for now. We'll work on making this usable for non zed.dev
users later

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-04-16 19:25:00 +02:00
5brian
2b277123be vim: Fix LineUp (#27754)
Closes #27423

Release Notes:

- vim: Fixed cursor scrolling off screen with `ctrl-y`.

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-04-16 17:24:24 +00:00
Marshall Bowers
bb0b2a5b7b collab: Remove has_predict_edits_feature_flag from LlmTokenClaims (#28879)
This PR removes the `has_predict_edits_feature_flag` field from the
`LlmTokenClaims`.

We are no longer reading this anywhere.

Release Notes:

- N/A
2025-04-16 17:11:18 +00:00
Piotr Osiewicz
5c2c6d7e5e toolchain: Respect currently focused file when querying toolchains (#28875)
Closes #21743


https://github.com/user-attachments/assets/0230f233-58a4-494c-90af-28ce82f9fc1d


Release Notes:

- Virtual environment picker now looks up virtual environment based on
parent directory of active file; this enables having multiple active
virtual environments in a single worktree.
2025-04-16 19:05:57 +02:00
Danilo Leal
4f58bdee28 agent: Add small design tweaks (#28874)
Some small adjustments to simplify the agent panel's design.

Release Notes:

- N/A
2025-04-16 13:03:36 -03:00
Kirill Bulatov
486a9e4d61 Fix more panics when removing excerpts (#28836)
Release Notes:

- Fixed a panic when an excerpt removed has an edit suggestion inlay in
it

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-04-16 15:33:28 +00:00
Smit Barmase
0d8f77b5de editor: Expand selection to word under cursor before expanding to next enclosing syntax node (#28864)
Closes #27995

For strings in any language and Markdown, `select_larger_syntax_node`
will first select the word and then expand from there if:
- The cursor is on the word.
- The selection is inside the word.

It will not select the word and will directly proceed to expand if:
- The word is already selected.
- Multiple partial words are selected.

Todo:
- [x] Tests

Release Notes:

- Fixed `select_larger_syntax_node` to first expand to the word within a
string, and then to the larger syntax node.
2025-04-16 20:52:26 +05:30
Marshall Bowers
cb79420773 agent: Show an error when the model requests limit has been reached (#28868)
This PR adds an error message when the model requests limit has been
hit.

Release Notes:

- N/A

Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
2025-04-16 15:11:35 +00:00
Thomas Mickley-Doyle
c641209341 agent: Add GitHub action for daily eval run (#28863)
Release Notes:

- N/A
2025-04-16 09:22:41 -05:00
Joseph T. Lyons
48a716fcb5 Bump Zed to v0.184 (#28861)
Release Notes:

-N/A
2025-04-16 13:06:43 +00:00
Piotr Osiewicz
25956c49c1 lsp: Register buffers with language server when querying inlay hints (#28855)
We register buffers with language servers lazily when in multi-buffer
(when the excerpt is interacted with); this does not account for inlay
hints, of which a mere presence on a screen is enough to query a
language server with a path it does not recognize. This posed a problem
with typescript-language-server, which sent a notification to the user
whenever they had a multibuffer open with inlay hints enabled.

Closes #ISSUE

Release Notes:

- Fixed annoying pop-up with typescript-language-server that happened in
multi-buffers with inlay hints enabled.
2025-04-16 11:05:53 +00:00
Anthony Eid
4efabe17dd debugger: Add Debug Panel context menu (#28847)
This PR adds a debug panel context menu that will allow a user to select
which debug session items are visible.

The context menu will add to the pane that was right clicked on.

<img width="1275" alt="Screenshot 2025-04-16 at 2 43 36 AM"
src="https://github.com/user-attachments/assets/330322ff-69db-4731-bbaf-3544d53f2f15"
/>


Release Notes:

- N/A
2025-04-16 08:36:51 +00:00
Michael Sloan
320abe9b22 Agent Eval: Check if SHA already fetched (#28846)
Release Notes:

- N/A
2025-04-16 06:54:22 +00:00
Michael Sloan
9a9f2e71ca Agent Eval: Initial support for running examples repeatedly (#28844)
Not ideal as it creates a separate worktree for each repetition

Release Notes:

- N/A
2025-04-16 06:35:55 +00:00
Michael Sloan
609895d95f Agent Eval: bounded concurrency (#28843)
Release Notes:

- N/A
2025-04-16 00:05:46 -06:00
Michael Sloan
da2d8bd845 Agent Eval: Distinguish tool successes and failures in log (#28839)
Release Notes:

- N/A
2025-04-15 22:51:33 -06:00
Conrad Irwin
6267a147ba Render error message (not pointer) (#28797)
Closes #ISSUE

Release Notes:

- N/A
2025-04-16 04:27:09 +00:00
Conrad Irwin
aceecec6bf Remove user agent from Git (#28798)
Closes #28629

Azure seems to break if this is set.

Release Notes:

- git: Stop sending a custom HTTP header on remote operations
2025-04-15 22:15:07 -06:00
Cole Miller
f3f2c6d811 Fix commondir discovery for git submodules (#28802)
The implementation of commondir discovery in #27885 was wrong, most
significantly for submodules but also for worktrees in rarer cases. The
correct procedure, implemented in this PR, is:

> If `.git` is a file, look at the `gitdir` it points to. If that
directory has a file called `commondir`, read that file to find the
commondir. (This is what happens for worktrees.) Otherwise, the
commondir is the same as the gitdir. (This is what happens for
submodules.)

Release Notes:

- N/A
2025-04-15 23:32:59 -04:00
Kirill Bulatov
41cffa64b0 Fix anchor comparison in multi buffer after expanding excerpts (#28828)
Release Notes:

- Fixed incorrect excerpt comparison when replacing them

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Conrad Irwin <conrad@zed.dev>
2025-04-15 21:30:11 -06:00
Marshall Bowers
b486e32f05 collab: Add GET /billing/usage endpoint (#28832)
This PR adds a `GET /billing/usage` endpoint for retrieving billing
usage to show on the `zed.dev/account` page.

Release Notes:

- N/A
2025-04-16 03:28:09 +00:00
Thomas Mickley-Doyle
222d4a2546 agent: Add telemetry for eval runs (#28816)
Release Notes:

- N/A

---------

Co-authored-by: Joseph <joseph@zed.dev>
2025-04-16 02:54:26 +00:00
Andy Waite
1eb948654a docs: Update Rails test task to run using name (#28574)
The author of Rails' minitest integration
[recommended](https://github.com/zed-extensions/ruby/issues/56#issuecomment-2795010202)
using the test name rather than line number.

This solves the problem in
https://github.com/zed-extensions/ruby/issues/56.

Note that everything is within `command`. I first tried using `args`:

```json
{
  "command": "bin/rails",
  "args": ["test", "$ZED_RELATIVE_FILE -n /$ZED_SYMBOL/"],
  "tags": ["ruby-test"]
}
```
but minitest receives this as:

```
Run options: -n "/\"foo bar\"/" --seed 31855
```

which doesn't match due to the escaping.

Release Notes:

- N/A
2025-04-15 21:39:32 -04:00
Finn Evers
35da1502e1 feedback: Update issue template URL (#28790)
Closes #28782 

The linked template path was updated in #28250. This PR also adds the
change to the zed action.

Since the issue template link was also referenced in workspace, I
updated that occurrence to use the `FileBugReport` action instead. For
that, I had to move the action to `zed_actions`. However, with this
change only one link has to be updated and any database related errors
will have the zed version specs attached to them automatically.

Release Notes:

- Fixed an issue where the `file bug report` action would redirect to an
outdated URL.
2025-04-15 21:36:30 -04:00
Ben Kunkle
1d98b33ae0 git_panel: Pad end of list to avoid obscuring final entry with horizontal scrollbar (#28823)
Closes #27406

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-04-16 01:02:53 +00:00
João Marcos
4e8ecfc0c4 Increase cx.condition timeout to fix flaky test (#28822)
We've been seeing `test_no_duplicated_completion_requests` fail randomly
with the error "condition timed out".

But it's always failing on MacOS, and MacOS sets a shorter timeout of
100ms, compared to 1s from other platforms, this PR increases MacOS's
timeout to match other platforms'.

Release Notes:

- N/A
2025-04-16 00:36:35 +00:00
Peter Tripp
134a0563c2 docs: Missing comma (#28780)
Release Notes:

- N/A
2025-04-16 00:30:59 +00:00
João Marcos
3f4d4af080 fix slicing crash in do_completion (#28820)
Release Notes:

- N/A
2025-04-15 23:37:37 +00:00
Marshall Bowers
68ec1d724c collab: Include subscription_period in LLM token claims (#28819)
This PR updates the LLM token claims to include the user's active
subscription period.

Release Notes:

- N/A
2025-04-15 23:25:41 +00:00
Michael Sloan
102ea6ac79 Add support for judge repetitions in eval (#28811)
Release Notes:

- N/A

---------

Co-authored-by: Thomas <thomas@zed.dev>
2025-04-15 23:18:02 +00:00
Conrad Irwin
5d3718df2d Diagnostics small fixes (#28817)
- **Clear diagnostics cache when toggling warnings**
- **Fix focus when first adding excerpts**

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-04-15 16:42:04 -06:00
Anthony Eid
f1f5d602fc debugger: Save debug session layout when changing focus or closing session (#28788)
This fixes a bug where resizing the panes wouldn't be serialized and
persist

Release Notes:

- N/A

Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
2025-04-15 18:25:50 -04:00
Marshall Bowers
60624d81ba collab: Add subscription_usages table (#28818)
This PR adds a new `subscription_usages` table to the LLM database.

We'll use this table to track usage by subscribers.

Records will be looked up using `(user_id, period_start_at,
period_end_at)` to find the record for a user's current subscription
period.

Release Notes:

- N/A
2025-04-15 18:13:29 -04:00
Danilo Leal
91755b2db1 agent: Add scrollbar to the settings view (#28814)
Release Notes:

- agent: Added a scrollbar to the panel settings view.
2025-04-15 18:25:19 -03:00
Anthony Eid
e34fee55a0 debugger: Fix Rust debugger runnable (#28801)
We ran the locator after configuring the debugger binary which cause the
binary to never use the configuration from the cargo locator. This PR
fixes this by correcting the order of configuration.


co-authored-by Anthony Eid <anthony@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: piotr <piotr@zed.dev>
2025-04-15 17:10:06 -04:00
Marshall Bowers
dad6067e18 collab: Add support for subscribing to Zed Pro trials (#28812)
This PR adds support for subscribing to Zed Pro trials (and then
upgrading from a trial to Zed Pro).

Release Notes:

- N/A
2025-04-15 20:49:16 +00:00
Smit Barmase
5619a3e618 editor: Fix bad hide_mouse_cursor call in find_all_references (#28810)
Release Notes:

- N/A
2025-04-16 02:02:54 +05:30
Antonio Scandurra
06ad45ce08 Fix rejecting multiple hunks in AgentDiff (#28806)
Release Notes:

- Fixed a bug that caused `Reject All` to not always reject _all_ the
hunks.

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-04-15 20:15:58 +00:00
Oleksiy Syvokon
7e6387052f docs: Add troubleshooting guide for Linux audio issues (#28803)
These steps solved audio issues on my system (Tuxedo OS), but should be
applicable to any PipeWire/PulseAudio system that has more than one
audio interface.

I suspect that enabling `rtc_use_pipewire` in [LiveKit SDK](0773bcec4e/webrtc-sys/libwebrtc/build_linux.sh (L105C1-L105C27))
could help as well, but I haven't tried it.

Release Notes:

- N/A
2025-04-15 18:45:25 +00:00
349 changed files with 11431 additions and 4983 deletions

1
.clinerules Symbolic link
View File

@@ -0,0 +1 @@
.rules

1
.cursorrules Symbolic link
View File

@@ -0,0 +1 @@
.rules

View File

@@ -10,7 +10,7 @@ runs:
cargo install cargo-nextest --locked
- name: Install Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "18"

View File

@@ -16,7 +16,7 @@ runs:
run: cargo install cargo-nextest --locked
- name: Install Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "18"

View File

@@ -519,7 +519,7 @@ jobs:
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Install Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "18"

View File

@@ -22,7 +22,7 @@ jobs:
version: 9
- name: Setup Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "20"
cache: "pnpm"

View File

@@ -18,7 +18,7 @@ jobs:
version: 9
- name: Setup Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "20"
cache: "pnpm"

View File

@@ -23,7 +23,7 @@ jobs:
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Install Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "18"

View File

@@ -71,7 +71,7 @@ jobs:
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Install Node
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "18"

View File

@@ -0,0 +1,28 @@
name: Run Eval Daily
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
jobs:
run_eval:
name: Run Eval
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Run cargo eval
run: cargo run -p eval

1
.gitignore vendored
View File

@@ -18,7 +18,6 @@
.venv
.vscode
.wrangler
/.direnv
/assets/*licenses.*
/crates/collab/seed.json
/crates/theme/schemas/theme.json

110
.rules Normal file
View File

@@ -0,0 +1,110 @@
# Rust coding guidelines
* Prioritize code correctness and clarity. Speed and efficiency are secondary priorities unless otherwise specified.
* Do not write organizational or comments that summarize the code. Comments should only be written in order to explain "why" the code is written in some way in the case there is a reason that is tricky / non-obvious.
* Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
* Avoid using functions that panic like `unwrap()`, instead use mechanisms like `?` to propagate errors.
* Be careful with operations like indexing which may panic if the indexes are out of bounds.
# GPUI
GPUI is a UI framework which also provides primitives for state and concurrency management.
## Context
Context types allow interaction with global state, windows, entities, and system services. They are typically passed to functions as the argument named `cx`. When a function takes callbacks they come after the `cx` parameter.
* `App` is the root context type, providing access to global state and read and update of entities.
* `Context<T>` is provided when updating an `Entity<T>`. This context dereferences into `App`, so functions which take `&App` can also take `&Context<T>`.
* `AsyncApp` and `AsyncWindowContext` are provided by `cx.spawn` and `cx.spawn_in`. These can be held across await points.
## `Window`
`Window` provides access to the state of an application window. It is passed to functions as an argument named `window` and comes before `cx` when present. It is used for managing focus, dispatching actions, directly drawing, getting user input state, etc.
## Entities
An `Entity<T>` is a handle to state of type `T`. With `thing: Entity<T>`:
* `thing.entity_id()` returns `EntityId`
* `thing.downgrade()` returns `WeakEntity<T>`
* `thing.read(cx: &App)` returns `&T`.
* `thing.read_with(cx, |thing: &T, cx: &App| ...)` returns the closure's return value.
* `thing.update(cx, |thing: &mut T, cx: &mut Context<T>| ...)` allows the closure to mutate the state, and provides a `Context<T>` for interacting with the entity. It returns the closure's return value.
* `thing.update_in(cx, |thing: &mut T, window: &mut Window, cx: &mut Context<T>| ...)` takes a `AsyncWindowContext` or `VisualTestContext`. It's the same as `update` while also providing the `Window`.
Within the closures, the inner `cx` provided to the closure must be used instead of the outer `cx` to avoid issues with multiple borrows.
Trying to update an entity while it's already being updated must be avoided as this will cause a panic.
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.
## Concurrency
All use of entities and UI rendering occurs on a single foreground thread.
`cx.spawn(async move |cx| ...)` runs an async closure on the foreground thread. Within the closure, `cx` is an async context like `AsyncApp` or `AsyncWindowContext`.
When the outer cx is a `Context<T>`, the use of `spawn` instead looks like `cx.spawn(async move |handle, cx| ...)`, where `handle: WeakEntity<T>`.
To do work on other threads, `cx.background_spawn(async move { ... })` is used. Often this background task is awaited on by a foreground task which uses the results to update state.
Both `cx.spawn` and `cx.background_spawn` return a `Task<R>`, which is a future that can be awaited upon. If this task is dropped, then its work is cancelled. To prevent this one of the following must be done:
* Awaiting the task in some other async context.
* Detaching the task via `task.detach()` or `task.detach_and_log_err(cx)`, allowing it to run indefinitely.
* Storing the task in a field, if the work should be halted when the struct is dropped.
A task which doesn't do anything but provide a value can be created with `Task::ready(value)`.
## Elements
The `Render` trait is used to render some state into an element tree that is laid out using flexbox layout. An `Entity<T>` where `T` implements `Render` is sometimes called a "view".
Example:
```
struct TextWithBorder(SharedString);
impl Render for TextWithBorder {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().border_1().child(self.0.clone())
}
}
```
Since `impl IntoElement for SharedString` exists, it can be used as an argument to `child`. `SharedString` is used to avoid copying strings, and is either an `&'static str` or `Arc<str>`.
UI components that are constructed just to be turned into elements can instead implement the `RenderOnce` trait, which is similar to `Render`, but its `render` method takes ownership of `self`. Types that implement this trait can use `#[derive(IntoElement)]` to use them directly as children.
The style methods on elements are similar to those used by Tailwind CSS.
If some attributes or children of an element tree are conditional, `.when(condition, |this| ...)` can be used to run the closure only when `condition` is true. Similarly, `.when_some(option, |this, value| ...)` runs the closure when the `Option` has a value.
## Input events
Input event handlers can be registered on an element via methods like `.on_click(|event, window, cx: &mut App| ...)`.
Often event handlers will want to update the entity that's in the current `Context<T>`. The `cx.listener` method provides this - its use looks like `.on_click(cx.listener(|this: &mut T, event, window, cx: &mut Context<T>| ...)`.
## Actions
Actions are dispatched via user keyboard interaction or in code via `window.dispatch_action(SomeAction.boxed_clone(), cx)` or `focus_handle.dispatch_action(&SomeAction, window, cx)`.
Actions which have no data inside are created and registered with the `actions!(some_namespace, [SomeAction, AnotherAction])` macro call.
Actions that do have data must implement `Clone, Default, PartialEq, Deserialize, JsonSchema` and can be registered with an `impl_actions!(some_namespace, [SomeActionWithData])` macro call.
Action handlers can be registered on an element via the event handler `.on_action(|action, window, cx| ...)`. Like other event handlers, this is often used with `cx.listener`.
## Notify
When a view's state has changed in a way that may affect its rendering, it should call `cx.notify()`. This will cause the view to be rerendered. It will also cause any observe callbacks registered for the entity with `cx.observe` to be called.
## Entity events
While updating an entity (`cx: Context<T>`), it can emit an event using `cx.emit(event)`. Entities register which events they can emit by declaring `impl EventEmittor<EventType> for EntityType {}`.
Other entities can then register a callback to handle these events by doing `cx.subscribe(other_entity, |this, other_entity, event, cx| ...)`. This will return a `Subscription` which deregisters the callback when dropped. Typically `cx.subscribe` happens when creating a new entity and the subscriptions are stored in a `_subscriptions: Vec<Subscription>` field.

1
.windsurfrules Symbolic link
View File

@@ -0,0 +1 @@
.rules

View File

@@ -45,5 +45,6 @@
"hard_tabs": false,
"formatter": "auto",
"remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true
"ensure_final_newline_on_save": true,
"file_scan_exclusions": ["crates/eval/worktrees/", "crates/eval/repos/"]
}

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
.rules

285
Cargo.lock generated
View File

@@ -125,6 +125,37 @@ dependencies = [
"workspace",
"workspace-hack",
"zed_actions",
"zed_llm_client",
]
[[package]]
name = "agent2"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant_tool",
"assistant_tools",
"chrono",
"client",
"collections",
"ctor",
"env_logger 0.11.8",
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"language_model",
"language_models",
"parking_lot",
"project",
"reqwest_client",
"schemars",
"serde",
"serde_json",
"settings",
"smol",
"thiserror 2.0.12",
"util",
]
[[package]]
@@ -324,7 +355,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.27.1",
"thiserror 2.0.12",
"workspace-hack",
]
@@ -337,9 +368,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4"
[[package]]
name = "anyhow"
version = "1.0.97"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "approx"
@@ -448,7 +479,6 @@ dependencies = [
"smol",
"tempfile",
"util",
"which 6.0.3",
"workspace-hack",
]
@@ -567,7 +597,7 @@ dependencies = [
"settings",
"smallvec",
"smol",
"strum",
"strum 0.27.1",
"telemetry_events",
"text",
"theme",
@@ -704,26 +734,35 @@ dependencies = [
"assistant_tool",
"chrono",
"collections",
"component",
"feature_flags",
"futures 0.3.31",
"gpui",
"html_to_markdown",
"http_client",
"indoc",
"itertools 0.14.0",
"language",
"language_model",
"linkme",
"open",
"pretty_assertions",
"project",
"rand 0.8.5",
"regex",
"schemars",
"serde",
"serde_json",
"settings",
"tree-sitter-rust",
"ui",
"unindent",
"util",
"web_search",
"workspace",
"workspace-hack",
"worktree",
"zed_llm_client",
]
[[package]]
@@ -1335,12 +1374,13 @@ dependencies = [
[[package]]
name = "aws-sdk-bedrockruntime"
version = "1.80.0"
version = "1.82.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ee8ef191b908d013659ca2c0670215f0c920c781998e1dc55904d6bdb73b51"
checksum = "8cb95f77abd4321348dd2f52a25e1de199732f54d2a35860ad20f5df21c66b44"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-sigv4",
"aws-smithy-async",
"aws-smithy-eventstream",
"aws-smithy-http",
@@ -1352,6 +1392,7 @@ dependencies = [
"bytes 1.10.1",
"fastrand 2.3.0",
"http 0.2.12",
"hyper 0.14.32",
"once_cell",
"regex-lite",
"tracing",
@@ -1881,7 +1922,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.27.1",
"thiserror 2.0.12",
"tokio",
"workspace-hack",
@@ -2715,9 +2756,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.35"
version = "4.5.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2725,9 +2766,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.35"
version = "4.5.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
dependencies = [
"anstream",
"anstyle",
@@ -3028,7 +3069,7 @@ dependencies = [
"settings",
"sha2",
"sqlx",
"strum",
"strum 0.27.1",
"subtle",
"supermaven_api",
"telemetry_events",
@@ -3048,6 +3089,7 @@ dependencies = [
"workspace",
"workspace-hack",
"worktree",
"zed_llm_client",
]
[[package]]
@@ -3360,7 +3402,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum",
"strum 0.27.1",
"task",
"theme",
"ui",
@@ -4477,7 +4519,7 @@ dependencies = [
"optfield",
"proc-macro2",
"quote",
"strum",
"strum 0.26.3",
"syn 2.0.100",
]
@@ -4886,6 +4928,7 @@ dependencies = [
"collections",
"context_server",
"dap",
"dirs 5.0.1",
"env_logger 0.11.8",
"extension",
"fs",
@@ -4907,39 +4950,11 @@ dependencies = [
"serde",
"settings",
"shellexpand 2.1.2",
"telemetry",
"toml 0.8.20",
"unindent",
"util",
"workspace-hack",
]
[[package]]
name = "evals"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"client",
"clock",
"collections",
"dap",
"env_logger 0.11.8",
"feature_flags",
"fs",
"gpui",
"http_client",
"language",
"languages",
"node_runtime",
"open_ai",
"project",
"reqwest_client",
"semantic_index",
"serde",
"serde_json",
"settings",
"smol",
"util",
"uuid",
"workspace-hack",
]
@@ -5119,7 +5134,7 @@ dependencies = [
"serde",
"settings",
"smallvec",
"strum",
"strum 0.27.1",
"telemetry",
"theme",
"ui",
@@ -5970,7 +5985,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
"strum",
"strum 0.27.1",
"telemetry",
"theme",
"time",
@@ -6063,7 +6078,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.27.1",
"workspace-hack",
]
@@ -6169,7 +6184,7 @@ dependencies = [
"slotmap",
"smallvec",
"smol",
"strum",
"strum 0.27.1",
"sum_tree",
"taffy",
"thiserror 2.0.12",
@@ -6186,6 +6201,7 @@ dependencies = [
"windows 0.61.1",
"windows-core 0.61.0",
"windows-numerics",
"windows-registry 0.5.1",
"workspace-hack",
"x11-clipboard",
"x11rb",
@@ -6817,7 +6833,7 @@ name = "icons"
version = "0.1.0"
dependencies = [
"serde",
"strum",
"strum 0.27.1",
"workspace-hack",
]
@@ -7085,16 +7101,16 @@ dependencies = [
"paths",
"pretty_assertions",
"serde",
"strum",
"strum 0.27.1",
"util",
"workspace-hack",
]
[[package]]
name = "indexmap"
version = "2.9.0"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@@ -7128,10 +7144,12 @@ dependencies = [
name = "inline_completion"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"language",
"project",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -7162,6 +7180,7 @@ dependencies = [
"workspace",
"workspace-hack",
"zed_actions",
"zed_llm_client",
"zeta",
]
@@ -7663,7 +7682,6 @@ dependencies = [
"http_client",
"icons",
"image",
"log",
"open_ai",
"parking_lot",
"proto",
@@ -7671,11 +7689,12 @@ dependencies = [
"serde",
"serde_json",
"smol",
"strum",
"strum 0.27.1",
"telemetry_events",
"thiserror 2.0.12",
"util",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -7731,7 +7750,7 @@ dependencies = [
"serde_json",
"settings",
"smol",
"strum",
"strum 0.27.1",
"theme",
"thiserror 2.0.12",
"tiktoken-rs",
@@ -7739,6 +7758,7 @@ dependencies = [
"ui",
"util",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -7900,9 +7920,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.171"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libdbus-sys"
@@ -8703,7 +8723,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.27.1",
"workspace-hack",
]
@@ -9550,7 +9570,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.27.1",
"workspace-hack",
]
@@ -10799,9 +10819,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.94"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
@@ -11744,6 +11764,7 @@ name = "remote_server"
version = "0.1.0"
dependencies = [
"anyhow",
"askpass",
"async-watch",
"backtrace",
"cargo_toml",
@@ -11940,7 +11961,7 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"windows-registry",
"windows-registry 0.2.0",
]
[[package]]
@@ -12129,7 +12150,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"strum",
"strum 0.27.1",
"tracing",
"util",
"workspace-hack",
@@ -12638,9 +12659,9 @@ dependencies = [
[[package]]
name = "sea-orm"
version = "1.1.8"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "013d6c9e421b9c44c6eb6ebbee283b2a2c90eab2682088c4a2449706a42d117f"
checksum = "21e61af841881c137d4bc8e0d8411cee9168548b404f9e4788e8af7e8f94bd4e"
dependencies = [
"async-stream",
"async-trait",
@@ -12657,7 +12678,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"strum",
"strum 0.26.3",
"thiserror 2.0.12",
"time",
"tracing",
@@ -12667,9 +12688,9 @@ dependencies = [
[[package]]
name = "sea-orm-macros"
version = "1.1.8"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31feeff3ad5e999c64b2b8fd30933b2871911567c4d62dcf70b3effd970c7891"
checksum = "d6b86e3e77b548e6c6c1f612a1ca024d557dffdb81b838bf482ad3222140c77b"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@@ -13253,9 +13274,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.15.0"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
dependencies = [
"serde",
]
@@ -13322,6 +13343,7 @@ dependencies = [
"fs",
"futures 0.3.31",
"gpui",
"indoc",
"parking_lot",
"paths",
"schemars",
@@ -13450,9 +13472,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -13463,10 +13485,11 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3"
dependencies = [
"base64 0.22.1",
"bigdecimal",
"bytes 1.10.1",
"chrono",
@@ -13487,7 +13510,6 @@ dependencies = [
"percent-encoding",
"rust_decimal",
"rustls 0.23.26",
"rustls-pemfile 2.2.0",
"serde",
"serde_json",
"sha2",
@@ -13504,9 +13526,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce"
dependencies = [
"proc-macro2",
"quote",
@@ -13517,9 +13539,9 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7"
dependencies = [
"dotenvy",
"either",
@@ -13543,9 +13565,9 @@ dependencies = [
[[package]]
name = "sqlx-mysql"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7"
dependencies = [
"atoi",
"base64 0.22.1",
@@ -13590,9 +13612,9 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6"
dependencies = [
"atoi",
"base64 0.22.1",
@@ -13633,9 +13655,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc"
dependencies = [
"atoi",
"chrono",
@@ -13651,6 +13673,7 @@ dependencies = [
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.12",
"time",
"tracing",
"url",
@@ -13702,7 +13725,7 @@ dependencies = [
"settings",
"simplelog",
"story",
"strum",
"strum 0.27.1",
"theme",
"title_bar",
"ui",
@@ -13784,7 +13807,16 @@ version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
"strum_macros 0.26.4",
]
[[package]]
name = "strum"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
dependencies = [
"strum_macros 0.27.1",
]
[[package]]
@@ -13800,6 +13832,19 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.100",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -14415,7 +14460,7 @@ dependencies = [
"serde_json_lenient",
"serde_repr",
"settings",
"strum",
"strum 0.27.1",
"thiserror 2.0.12",
"util",
"uuid",
@@ -14449,7 +14494,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"simplelog",
"strum",
"strum 0.27.1",
"theme",
"vscode_theme",
"workspace-hack",
@@ -15450,7 +15495,7 @@ dependencies = [
"settings",
"smallvec",
"story",
"strum",
"strum 0.27.1",
"theme",
"ui_macros",
"util",
@@ -15824,7 +15869,6 @@ dependencies = [
"indoc",
"itertools 0.14.0",
"language",
"libc",
"log",
"lsp",
"multi_buffer",
@@ -16583,6 +16627,36 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web_search"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"gpui",
"serde",
"workspace-hack",
"zed_llm_client",
]
[[package]]
name = "web_search_providers"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
"language_model",
"serde",
"serde_json",
"web_search",
"workspace-hack",
"zed_llm_client",
]
[[package]]
name = "webpki-root-certs"
version = "0.26.8"
@@ -17001,6 +17075,17 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-registry"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
dependencies = [
"windows-link",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
[[package]]
name = "windows-result"
version = "0.1.2"
@@ -17621,7 +17706,7 @@ dependencies = [
"settings",
"smallvec",
"sqlez",
"strum",
"strum 0.27.1",
"task",
"telemetry",
"tempfile",
@@ -17766,7 +17851,7 @@ dependencies = [
"sqlx-macros-core",
"sqlx-postgres",
"sqlx-sqlite",
"strum",
"strum 0.26.3",
"subtle",
"syn 1.0.109",
"syn 2.0.100",
@@ -18138,12 +18223,13 @@ dependencies = [
[[package]]
name = "zed"
version = "0.183.0"
version = "0.184.0"
dependencies = [
"activity_indicator",
"agent",
"anyhow",
"ashpd",
"askpass",
"assets",
"assistant",
"assistant_context_editor",
@@ -18261,6 +18347,8 @@ dependencies = [
"uuid",
"vim",
"vim_mode_setting",
"web_search",
"web_search_providers",
"welcome",
"windows 0.61.1",
"winresource",
@@ -18279,6 +18367,7 @@ dependencies = [
"gpui",
"schemars",
"serde",
"uuid",
"workspace-hack",
]
@@ -18325,12 +18414,14 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.4.1"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf21350eced858d129840589158a8f6895c4fa4327ae56dd8c7d6a98495bed4"
checksum = "ad17428120f5ca776dc5195e2411a282f5150a26d5536671f8943c622c31274f"
dependencies = [
"anyhow",
"serde",
"serde_json",
"strum 0.27.1",
"uuid",
]

View File

@@ -3,6 +3,7 @@ resolver = "2"
members = [
"crates/activity_indicator",
"crates/agent",
"crates/agent2",
"crates/anthropic",
"crates/askpass",
"crates/assets",
@@ -47,7 +48,6 @@ members = [
"crates/docs_preprocessor",
"crates/editor",
"crates/eval",
"crates/evals",
"crates/extension",
"crates/extension_api",
"crates/extension_cli",
@@ -165,6 +165,8 @@ members = [
"crates/util_macros",
"crates/vim",
"crates/vim_mode_setting",
"crates/web_search",
"crates/web_search_providers",
"crates/welcome",
"crates/workspace",
"crates/worktree",
@@ -370,6 +372,8 @@ util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
web_search = { path = "crates/web_search" }
web_search_providers = { path = "crates/web_search_providers" }
welcome = { path = "crates/welcome" }
workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
@@ -536,7 +540,7 @@ smol = "2.0"
sqlformat = "0.2"
streaming-iterator = "0.1"
strsim = "0.11"
strum = { version = "0.26.0", features = ["derive"] }
strum = { version = "0.27.0", features = ["derive"] }
subtle = "2.5.0"
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
sys-locale = "0.3.1"
@@ -601,7 +605,7 @@ wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.4"
zed_llm_client = "0.6.1"
zstd = "0.11"
metal = "0.29"
@@ -692,7 +696,6 @@ breadcrumbs = { codegen-units = 1 }
collections = { codegen-units = 1 }
command_palette = { codegen-units = 1 }
command_palette_hooks = { codegen-units = 1 }
evals = { codegen-units = 1 }
extension_cli = { codegen-units = 1 }
feature_flags = { codegen-units = 1 }
file_icons = { codegen-units = 1 }

View File

@@ -49,15 +49,6 @@
"down": "menu::SelectNext"
}
},
{
"context": "Prompt",
"bindings": {
"left": "menu::SelectPrevious",
"right": "menu::SelectNext",
"h": "menu::SelectPrevious",
"l": "menu::SelectNext"
}
},
{
"context": "Editor",
"bindings": {
@@ -134,25 +125,9 @@
"shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint"
}
},
{
"context": "Editor && !agent_diff",
"bindings": {
"ctrl-k ctrl-r": "git::Restore",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AgentDiff",
"bindings": {
"ctrl-y": "agent::Keep",
"ctrl-n": "agent::Reject",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
"shift-f9": "editor::EditLogBreakpoint",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
}
},
{
@@ -203,6 +178,31 @@
"ctrl-c": "markdown::Copy"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
}
},
{
"context": "Editor && !agent_diff",
"bindings": {
"ctrl-k ctrl-r": "git::Restore",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext"
}
},
{
"context": "AgentDiff",
"bindings": {
"ctrl-y": "agent::Keep",
"ctrl-n": "agent::Reject",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "AssistantPanel",
"bindings": {
@@ -218,6 +218,93 @@
"ctrl-n": "assistant::NewChat"
}
},
{
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-shift-enter": "assistant::Edit",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentPanel",
"bindings": {
"ctrl-n": "agent::NewThread",
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
"ctrl-alt-p": "assistant::OpenPromptLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"shift-escape": "agent::ExpandMessageEditor",
"ctrl-e": "agent::ChatMode",
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
{
"context": "AgentPanel && prompt_editor",
"bindings": {
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"bindings": {
"enter": "agent::Chat",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
"context": "EditMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "ThreadHistory",
"bindings": {
"backspace": "agent::RemoveSelectedThread"
}
},
{
"context": "PromptLibrary",
"bindings": {
@@ -600,99 +687,6 @@
"ctrl-:": "editor::ToggleInlayHints"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
}
},
{
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-shift-enter": "assistant::Edit",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentPanel",
"bindings": {
"ctrl-n": "agent::NewThread",
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"shift-escape": "agent::ExpandMessageEditor",
"ctrl-e": "agent::ChatMode",
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
{
"context": "AgentPanel && prompt_editor",
"bindings": {
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
}
},
{
"context": "MessageEditor > Editor",
"bindings": {
"enter": "agent::Chat",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
"context": "EditMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "ThreadHistory",
"bindings": {
"backspace": "agent::RemoveSelectedThread"
}
},
{
"context": "PromptEditor",
"bindings": {
@@ -701,6 +695,15 @@
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
{
"context": "Prompt",
"bindings": {
"left": "menu::SelectPrevious",
"right": "menu::SelectNext",
"h": "menu::SelectPrevious",
"l": "menu::SelectNext"
}
},
{
"context": "ProjectSearchBar && !in_replace",
"bindings": {
@@ -718,7 +721,7 @@
"alt-shift-copy": "workspace::CopyRelativePath",
"alt-ctrl-shift-c": "workspace::CopyRelativePath",
"alt-ctrl-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::Open",
"space": "outline_panel::OpenSelectedEntry",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"alt-enter": "editor::OpenExcerpts",
@@ -917,6 +920,7 @@
"ctrl-enter": "assistant::InlineAssist",
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
// Overrides for conflicting keybindings
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],

View File

@@ -286,6 +286,7 @@
"cmd-alt-n": "agent::NewTextThread",
"cmd-shift-h": "agent::OpenHistory",
"cmd-alt-c": "agent::OpenConfiguration",
"cmd-alt-p": "assistant::OpenPromptLibrary",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-shift-a": "agent::ToggleContextPicker",
@@ -541,7 +542,9 @@
"cmd-\\": "pane::SplitRight",
"cmd-k v": "markdown::OpenPreviewToTheSide",
"cmd-shift-v": "markdown::OpenPreview",
"ctrl-cmd-c": "editor::DisplayCursorNames"
"ctrl-cmd-c": "editor::DisplayCursorNames",
"cmd-shift-backspace": "editor::GoToPreviousChange",
"cmd-shift-alt-backspace": "editor::GoToNextChange"
}
},
{
@@ -786,7 +789,7 @@
"cmd-alt-c": "workspace::CopyPath",
"alt-cmd-shift-c": "workspace::CopyRelativePath",
"alt-cmd-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::Open",
"space": "outline_panel::OpenSelectedEntry",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"alt-enter": "editor::OpenExcerpts",
@@ -1002,6 +1005,7 @@
"alt-right": ["terminal::SendText", "\u001bf"],
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
// There are conflicting bindings for these keys in the global context.
// these bindings override them, remove at your own risk:
"up": ["terminal::SendKeystroke", "up"],

View File

@@ -190,7 +190,8 @@
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
"ctrl-w space": "editor::OpenExcerptsSplit",
"ctrl-w g space": "editor::OpenExcerptsSplit",
"ctrl-6": "pane::AlternateFile"
"ctrl-6": "pane::AlternateFile",
"ctrl-^": "pane::AlternateFile"
}
},
{
@@ -786,8 +787,7 @@
"{": "project_panel::SelectPrevDirectory",
"shift-g": "menu::SelectLast",
"g g": "menu::SelectFirst",
"-": "project_panel::SelectParent",
"ctrl-6": "pane::AlternateFile"
"-": "project_panel::SelectParent"
}
},
{

View File

@@ -1,162 +1,71 @@
You are an AI assistant integrated into a code editor. You have the programming ability of an expert programmer who takes pride in writing high-quality code and is driven to the point of obsession about solving problems effectively. Your goal is to do one of the following two things:
You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
1. Help users answer questions and perform tasks related to their codebase.
2. Answer general-purpose questions unrelated to their particular codebase.
## Communication
It will be up to you to decide which of these you are doing based on what the user has told you. When unclear, ask clarifying questions to understand the user's intent before proceeding.
1. Be conversational but professional.
2. Refer to the USER in the second person and yourself in the first person.
3. Format your responses in markdown. Use backticks to format file, directory, function, and class names.
4. NEVER lie or make things up.
5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing.
You should only perform actions that modify the user's system if explicitly requested by the user:
- If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the user's system without explicit instruction.
- If the user clearly requests that you perform an action, carry out the action directly without explaining why you are doing so.
## Tool Use
When answering questions, it's okay to give incomplete examples containing comments about what would go there in a real version. When being asked to directly perform tasks on the code base, you must ALWAYS make fully working code. You may never "simplify" the code by omitting or deleting functionality you know the user has requested, and you must NEVER write comments like "in a full version, this would..." - instead, you must actually implement the real version. Don't be lazy!
1. Make sure to adhere to the tools schema.
2. Provide every required argument.
3. DO NOT use tools to access items that are already available in the context section.
4. Use only the tools that are currently available.
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
Note that project files are automatically backed up. The user can always get them back later if anything goes wrong, so there's
no need to create backup files (e.g. `.bak` files) because these files will just take up unnecessary space on the user's disk.
## Searching and Reading
When attempting to resolve issues around failing tests, never simply remove the failing tests. Unless the user explicitly asks you to remove tests, ALWAYS attempt to fix the code causing the tests to fail.
If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions.
Ignore "TODO"-type comments unless they're relevant to the user's explicit request or the user specifically asks you to address them. It is, however, okay to include them in codebase summaries.
<style>
Editing code:
- Make sure to take previous edits into account.
- The edits you perform might lead to errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made.
- You may only attempt to fix these up to 3 times. If you have tried 3 times to fix them, and there are still problems remaining, you must not continue trying to fix them, and must instead tell the user that there are problems remaining - and ask if the user would like you to attempt to solve them further.
- Do not fix errors unrelated to your changes unless the user explicitly asks you to do so.
- Prefer to move files over recreating them. The move can be followed by minor edits if required.
- If you seem to be stuck, never go back and "simplify the implementation" by deleting the parts of the implementation you're stuck on and replacing them with comments. If you ever feel the urge to do this, instead immediately stop whatever you're doing (even if the code is in a broken state), report that you are stuck, explain what you're stuck on, and ask the user how to proceed.
Tool use:
- Make sure to adhere to the tools schema.
- Provide every required argument.
- DO NOT use tools to access items that are already available in the context section.
- Use only the tools that are currently available.
- DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
Responding:
- Be concise and direct in your responses.
- Never apologize or thank the user.
- Don't comment that you have just realized or understood something.
- When you are going to make a tool call, tersely explain your reasoning for choosing to use that tool, with no flourishes or commentary beyond that information.
For example, rather than saying "You're absolutely right! Thank you for providing that context. Now I understand that we're missing a dependency, and I need to add it:" say "I'll add that missing dependency:" instead.
- Also, don't restate what a tool call is about to do (or just did).
For example, don't say "Now I'm going to check diagnostics to see if there are any warnings or errors," followed by running a tool which checks diagnostics and reports warnings or errors; instead, just request the tool call without saying anything.
- All tool results are provided to you automatically, so DO NOT thank the user when this happens.
Whenever you mention a code block, you MUST use ONLY the following format:
```language path/to/Something.blah#L123-456
(code goes here)
```
The `#L123-456` means the line number range 123 through 456, and the path/to/Something.blah
is a path in the project. (If there is no valid path in the project, then you can use
/dev/null/path.extension for its path.) This is the ONLY valid way to format code blocks, because the Markdown parser
does not understand the more common ```language syntax, or bare ``` blocks. It only
understands this path-based syntax, and if the path is missing, then it will error and you will have to do it over again.
Just to be really clear about this, if you ever find yourself writing three backticks followed by a language name, STOP!
You have made a mistake. You can only ever put paths after triple backticks!
<example>
Based on all the information I've gathered, here's a summary of how this system works:
1. The README file is loaded into the system.
2. The system finds the first two headers, including everything in between. In this case, that would be:
```path/to/README.md#L8-12
# First Header
This is the info under the first header.
## Sub-header
```
3. Then the system finds the last header in the README:
```path/to/README.md#L27-29
## Last Header
This is the last header in the README.
```
4. Finally, it passes this information on to the next process.
</example>
<example>
In Markdown, hash marks signify headings. For example:
```/dev/null/example.md#L1-3
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</example>
Here are examples of ways you must never render code blocks:
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
```
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</bad_example_do_not_do_this>
This example is unacceptable because it does not include the path.
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
```markdown
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</bad_example_do_not_do_this>
This example is unacceptable because it has the language instead of the path.
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
# Level 1 heading
## Level 2 heading
### Level 3 heading
</bad_example_do_not_do_this>
This example is unacceptable because it uses indentation to mark the code block
instead of backticks with a path.
<bad_example_do_not_do_this>
In Markdown, hash marks signify headings. For example:
```markdown
/dev/null/example.md#L1-3
# Level 1 heading
## Level 2 heading
### Level 3 heading
```
</bad_example_do_not_do_this>
This example is unacceptable because the path is in the wrong place. The path must be directly after the opening backticks.
</style>
The user has opened a project that contains the following root directories/files. Whenever you specify a path in the project, it must be a relative path which begins with one of these root directories/files:
{{! TODO: If there are files, we should mention it but otherwise omit that fact }}
If appropriate, use tool calls to explore the current project, which contains the following root directories:
{{#each worktrees}}
- `{{root_name}}` (absolute path: `{{abs_path}}`)
- `{{root_name}}`
{{/each}}
{{#if has_rules}}
There are rules that apply to these root directories:
- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.
- When looking for symbols in the project, prefer the `grep` tool.
- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project.
- Bias towards not asking the user for help if you can find the answer yourself.
## Fixing Diagnostics
1. Make 1-2 attempts at fixing diagnostics, then defer to the user.
2. Never simplify code you've written just to solve diagnostics. Complete, mostly correct code is more valuable than perfect code that doesn't solve the problem.
## Debugging
When debugging, only make code changes if you are certain that you can solve the problem.
Otherwise, follow debugging best practices:
1. Address the root cause instead of the symptoms.
2. Add descriptive logging statements and error messages to track variable and code state.
3. Add test functions and statements to isolate the problem.
## Calling External APIs
1. Unless explicitly requested by the user, use the best suited external APIs and packages to solve the task. There is no need to ask the user for permission.
2. When selecting which version of an API or package to use, choose one that is compatible with the user's dependency management file. If no such file exists or if the package is not present, use the latest version that is in your training data.
3. If an external API requires an API Key, be sure to point this out to the user. Adhere to best security practices (e.g. DO NOT hardcode an API key in a place where it can be exposed)
## System Information
Operating System: {{os}}
Default Shell: {{shell}}
{{#if (or has_rules has_default_user_rules)}}
## User's Custom Instructions
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the tool use guidelines.
{{#if has_rules}}
There are project rules that apply to these root directories:
{{#each worktrees}}
{{#if rules_file}}
`{{root_name}}/{{rules_file.path_in_worktree}}`:
``````
{{{rules_file.text}}}
``````
@@ -164,7 +73,16 @@ There are rules that apply to these root directories:
{{/each}}
{{/if}}
<user_environment>
Operating System: {{os}} ({{arch}})
Shell: {{shell}}
</user_environment>
{{#if has_default_user_rules}}
The user has specified the following rules that should be applied:
{{#each default_user_rules}}
{{#if title}}
Rules title: {{title}}
{{/if}}
``````
{{contents}}}
``````
{{/each}}
{{/if}}
{{/if}}

View File

@@ -181,8 +181,6 @@
"current_line_highlight": "all",
// Whether to highlight all occurrences of the selected text in an editor.
"selection_highlight": true,
// The debounce delay before querying highlights based on the selected text.
"selection_highlight_debounce": 50,
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
@@ -587,7 +585,6 @@
//
// Default: main
"fallback_branch_name": "main",
"scrollbar": {
// When to show the scrollbar in the git panel.
//
@@ -651,34 +648,36 @@
"now": true,
"path_search": true,
"read_file": true,
"regex_search": true,
"thinking": true
"grep": true,
"thinking": true,
"web_search": true
}
},
"write": {
"name": "Write",
"enable_all_context_servers": true,
"tools": {
"terminal": true,
"batch_tool": true,
"code_actions": true,
"code_symbols": true,
"contents": true,
"batch_tool": false,
"code_actions": false,
"code_symbols": false,
"contents": false,
"copy_path": false,
"create_file": true,
"delete_path": false,
"diagnostics": true,
"find_replace_file": true,
"edit_file": true,
"fetch": true,
"list_directory": false,
"list_directory": true,
"move_path": false,
"now": true,
"now": false,
"path_search": true,
"read_file": true,
"regex_search": true,
"rename": true,
"symbol_info": true,
"thinking": true
"grep": true,
"rename": false,
"symbol_info": false,
"terminal": true,
"thinking": true,
"web_search": true
}
}
},
@@ -1560,7 +1559,6 @@
// }
// ]
"ssh_connections": [],
// Configures context servers for use in the Assistant.
"context_servers": {},
"debugger": {

View File

@@ -90,6 +90,7 @@ uuid.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }

View File

@@ -1,27 +1,31 @@
use crate::context::{AssistantContext, ContextId};
use crate::context::{AssistantContext, ContextId, format_context_as_string};
use crate::context_picker::MentionLink;
use crate::thread::{
LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
ThreadEvent, ThreadFeedback,
LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent,
ThreadFeedback,
};
use crate::thread_store::{RulesLoadingError, ThreadStore};
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
use crate::tool_use::{PendingToolUseStatus, ToolUse};
use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
use crate::{AssistantPanel, OpenActiveThreadAsMarkdown};
use anyhow::Context as _;
use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
use assistant_tool::ToolUseStatus;
use collections::{HashMap, HashSet};
use editor::scroll::Autoscroll;
use editor::{Editor, EditorElement, EditorStyle, MultiBuffer};
use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer};
use gpui::{
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardItem,
DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Hsla, ListAlignment, ListState,
MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription, Task,
TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla, ListAlignment,
ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription,
Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
linear_color_stop, linear_gradient, list, percentage, pulsating_between,
};
use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role, StopReason};
use language_model::{
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolUseId, RequestUsage, Role,
StopReason,
};
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
use project::ProjectItem as _;
@@ -38,6 +42,7 @@ use ui::{
};
use util::ResultExt as _;
use workspace::{OpenOptions, Workspace};
use zed_actions::assistant::OpenPromptLibrary;
use crate::context_store::ContextStore;
@@ -60,6 +65,7 @@ pub struct ActiveThread {
expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
expanded_code_blocks: HashMap<(MessageId, usize), bool>,
last_error: Option<ThreadError>,
last_usage: Option<RequestUsage>,
notifications: Vec<WindowHandle<AgentNotification>>,
copied_code_block_ids: HashSet<(MessageId, usize)>,
_subscriptions: Vec<Subscription>,
@@ -127,18 +133,23 @@ impl RenderedMessage {
}
fn push_segment(&mut self, segment: &MessageSegment, cx: &mut App) {
let rendered_segment = match segment {
MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {
content: parse_markdown(text.into(), self.language_registry.clone(), cx),
scroll_handle: ScrollHandle::default(),
},
MessageSegment::Text(text) => RenderedMessageSegment::Text(parse_markdown(
text.into(),
self.language_registry.clone(),
cx,
)),
match segment {
MessageSegment::Thinking { text, .. } => {
self.segments.push(RenderedMessageSegment::Thinking {
content: parse_markdown(text.into(), self.language_registry.clone(), cx),
scroll_handle: ScrollHandle::default(),
})
}
MessageSegment::Text(text) => {
self.segments
.push(RenderedMessageSegment::Text(parse_markdown(
text.into(),
self.language_registry.clone(),
cx,
)))
}
MessageSegment::RedactedThinking(_) => {}
};
self.segments.push(rendered_segment);
}
}
@@ -497,12 +508,13 @@ fn render_markdown_code_block(
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
let codeblock_header = h_flex()
.group("codeblock_header")
.p_1()
.py_1()
.pl_1p5()
.pr_1()
.gap_1()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(codeblock_header_bg)
.rounded_t_md()
.children(label)
@@ -510,7 +522,7 @@ fn render_markdown_code_block(
h_flex()
.gap_1()
.child(
div().visible_on_hover("codeblock_header").child(
div().visible_on_hover("codeblock_container").child(
IconButton::new(
("copy-markdown-code", ix),
if codeblock_was_copied {
@@ -592,11 +604,12 @@ fn render_markdown_code_block(
);
v_flex()
.group("codeblock_container")
.my_2()
.overflow_hidden()
.rounded_lg()
.border_1()
.border_color(cx.theme().colors().border_variant)
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(cx.theme().colors().editor_background)
.child(codeblock_header)
.when(
@@ -681,6 +694,9 @@ fn open_markdown_link(
struct EditMessageState {
editor: Entity<Editor>,
last_estimated_token_count: Option<usize>,
_subscription: Subscription,
_update_token_count_task: Option<Task<anyhow::Result<()>>>,
}
impl ActiveThread {
@@ -726,6 +742,7 @@ impl ActiveThread {
hide_scrollbar_task: None,
editing_message: None,
last_error: None,
last_usage: None,
copied_code_block_ids: HashSet::default(),
notifications: Vec::new(),
_subscriptions: subscriptions,
@@ -750,6 +767,10 @@ impl ActiveThread {
this
}
pub fn context_store(&self) -> &Entity<ContextStore> {
&self.context_store
}
pub fn thread(&self) -> &Entity<Thread> {
&self.thread
}
@@ -780,6 +801,17 @@ impl ActiveThread {
self.last_error.take();
}
pub fn last_usage(&self) -> Option<RequestUsage> {
self.last_usage
}
/// Returns the editing message id and the estimated token count in the content
pub fn editing_message_id(&self) -> Option<(MessageId, usize)> {
self.editing_message
.as_ref()
.map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0)))
}
fn push_message(
&mut self,
id: &MessageId,
@@ -857,6 +889,9 @@ impl ActiveThread {
ThreadEvent::ShowError(error) => {
self.last_error = Some(error.clone());
}
ThreadEvent::UsageUpdated(usage) => {
self.last_usage = Some(*usage);
}
ThreadEvent::StreamedCompletion
| ThreadEvent::SummaryGenerated
| ThreadEvent::SummaryChanged => {
@@ -943,8 +978,8 @@ impl ActiveThread {
&tool_use.input,
self.thread
.read(cx)
.tool_result(&tool_use.id)
.map(|result| result.content.clone().into())
.output_for_tool(&tool_use.id)
.map(|output| output.clone().into())
.unwrap_or("".into()),
cx,
);
@@ -1125,15 +1160,93 @@ impl ActiveThread {
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
editor
});
let subscription = cx.subscribe(&editor, |this, _, event, cx| match event {
EditorEvent::BufferEdited => {
this.update_editing_message_token_count(true, cx);
}
_ => {}
});
self.editing_message = Some((
message_id,
EditMessageState {
editor: editor.clone(),
last_estimated_token_count: None,
_subscription: subscription,
_update_token_count_task: None,
},
));
self.update_editing_message_token_count(false, cx);
cx.notify();
}
fn update_editing_message_token_count(&mut self, debounce: bool, cx: &mut Context<Self>) {
let Some((message_id, state)) = self.editing_message.as_mut() else {
return;
};
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
state._update_token_count_task.take();
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
state.last_estimated_token_count.take();
return;
};
let editor = state.editor.clone();
let thread = self.thread.clone();
let message_id = *message_id;
state._update_token_count_task = Some(cx.spawn(async move |this, cx| {
if debounce {
cx.background_executor()
.timer(Duration::from_millis(200))
.await;
}
let token_count = if let Some(task) = cx.update(|cx| {
let context = thread.read(cx).context_for_message(message_id);
let new_context = thread.read(cx).filter_new_context(context);
let context_text =
format_context_as_string(new_context, cx).unwrap_or(String::new());
let message_text = editor.read(cx).text(cx);
let content = context_text + &message_text;
if content.is_empty() {
return None;
}
let request = language_model::LanguageModelRequest {
thread_id: None,
prompt_id: None,
messages: vec![LanguageModelRequestMessage {
role: language_model::Role::User,
content: vec![content.into()],
cache: false,
}],
tools: vec![],
stop: vec![],
temperature: None,
};
Some(default_model.model.count_tokens(request, cx))
})? {
task.await?
} else {
0
};
this.update(cx, |this, cx| {
let Some((_message_id, state)) = this.editing_message.as_mut() else {
return;
};
state.last_estimated_token_count = Some(token_count);
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
})
}));
}
fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
self.editing_message.take();
cx.notify();
@@ -1171,7 +1284,8 @@ impl ActiveThread {
}
self.thread.update(cx, |thread, cx| {
thread.send_to_model(model.model, RequestKind::Chat, cx)
thread.advance_prompt_id();
thread.send_to_model(model.model, cx)
});
cx.notify();
}
@@ -1402,16 +1516,23 @@ impl ActiveThread {
let editor_bg_color = colors.editor_background;
let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileCode)
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileCode)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
.tooltip(Tooltip::text("Open Thread as Markdown"))
.on_click(|_event, window, cx| {
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(OpenActiveThreadAsMarkdown), cx)
});
let feedback_container = h_flex().py_2().px_4().gap_1().justify_between();
// For all items that should be aligned with the Assistant's response.
const RESPONSE_PADDING_X: Pixels = px(18.);
let feedback_container = h_flex()
.py_2()
.px(RESPONSE_PADDING_X)
.gap_1()
.justify_between();
let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
Some(feedback) => feedback_container
.child(
@@ -1612,9 +1733,9 @@ impl ActiveThread {
this.pt_4()
}
})
.pb_4()
.pl_2()
.pr_2p5()
.pb_4()
.child(
v_flex()
.bg(colors.editor_background)
@@ -1675,6 +1796,9 @@ impl ActiveThread {
"confirm-edit-message",
"Regenerate",
)
.disabled(
edit_message_editor.read(cx).is_empty(cx),
)
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
@@ -1718,9 +1842,8 @@ impl ActiveThread {
),
Role::Assistant => v_flex()
.id(("message-container", ix))
.ml_2p5()
.pl_2()
.pr_4()
.px(RESPONSE_PADDING_X)
.gap_2()
.children(message_content)
.when(has_tool_uses, |parent| {
parent.children(
@@ -1737,6 +1860,15 @@ impl ActiveThread {
),
};
let after_editing_message = self
.editing_message
.as_ref()
.map_or(false, |(editing_message_id, _)| {
message_id > *editing_message_id
});
let panel_background = cx.theme().colors().panel_background;
v_flex()
.w_full()
.when_some(checkpoint, |parent, checkpoint| {
@@ -1900,6 +2032,18 @@ impl ActiveThread {
},
)
})
.when(after_editing_message, |parent| {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(
div()
.occlude()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8),
)
})
.into_any()
}
@@ -1926,6 +2070,15 @@ impl ActiveThread {
None
};
let message_role = self
.thread
.read(cx)
.message(message_id)
.map(|m| m.role)
.unwrap_or(Role::User);
let is_assistant = message_role == Role::Assistant;
v_flex()
.text_ui(cx)
.gap_2()
@@ -1946,80 +2099,100 @@ impl ActiveThread {
cx,
)
.into_any_element(),
RenderedMessageSegment::Text(markdown) => div()
.child(
MarkdownElement::new(
markdown.clone(),
default_markdown_style(window, cx),
)
.code_block_renderer(markdown::CodeBlockRenderer::Custom {
render: Arc::new({
let workspace = workspace.clone();
let active_thread = cx.entity();
move |kind, parsed_markdown, range, metadata, window, cx| {
render_markdown_code_block(
message_id,
range.start,
kind,
parsed_markdown,
metadata,
active_thread.clone(),
workspace.clone(),
window,
cx,
)
}
}),
transform: Some(Arc::new({
let active_thread = cx.entity();
move |el, range, metadata, _, cx| {
let is_expanded = active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, range.start))
.copied()
.unwrap_or(false);
RenderedMessageSegment::Text(markdown) => {
let markdown_element = MarkdownElement::new(
markdown.clone(),
default_markdown_style(window, cx),
);
if is_expanded
|| metadata.line_count
<= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK
{
return el;
let markdown_element = if is_assistant {
markdown_element.code_block_renderer(
markdown::CodeBlockRenderer::Custom {
render: Arc::new({
let workspace = workspace.clone();
let active_thread = cx.entity();
move |kind,
parsed_markdown,
range,
metadata,
window,
cx| {
render_markdown_code_block(
message_id,
range.start,
kind,
parsed_markdown,
metadata,
active_thread.clone(),
workspace.clone(),
window,
cx,
)
}
el.child(
div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_1_4()
.rounded_b_lg()
.bg(gpui::linear_gradient(
0.,
gpui::linear_color_stop(
cx.theme().colors().editor_background,
}),
transform: Some(Arc::new({
let active_thread = cx.entity();
move |el, range, metadata, _, cx| {
let is_expanded = active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, range.start))
.copied()
.unwrap_or(false);
if is_expanded
|| metadata.line_count
<= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK
{
return el;
}
el.child(
div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_1_4()
.rounded_b_lg()
.bg(gpui::linear_gradient(
0.,
),
gpui::linear_color_stop(
cx.theme()
.colors()
.editor_background
.opacity(0.),
1.,
),
)),
)
}
})),
})
.on_url_click({
gpui::linear_color_stop(
cx.theme()
.colors()
.editor_background,
0.,
),
gpui::linear_color_stop(
cx.theme()
.colors()
.editor_background
.opacity(0.),
1.,
),
)),
)
}
})),
},
)
} else {
markdown_element.code_block_renderer(
markdown::CodeBlockRenderer::Default {
copy_button: false,
border: true,
},
)
};
div()
.child(markdown_element.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
}
}),
)
.into_any_element(),
}))
.into_any_element()
}
},
),
)
@@ -2279,6 +2452,10 @@ impl ActiveThread {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
return card.render(&tool_use.status, window, cx);
}
let is_open = self
.expanded_tool_uses
.get(&tool_use.id)
@@ -2343,6 +2520,10 @@ impl ActiveThread {
rendered.input.clone(),
tool_use_markdown_style(window, cx),
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
border: false,
})
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
@@ -2369,12 +2550,17 @@ impl ActiveThread {
rendered.output.clone(),
tool_use_markdown_style(window, cx),
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
border: false,
})
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
}
})
.into_any_element()
}),
)),
),
@@ -2431,6 +2617,7 @@ impl ActiveThread {
open_markdown_link(text, workspace.clone(), window, cx);
}
})
.into_any_element()
})),
),
),
@@ -2468,11 +2655,10 @@ impl ActiveThread {
))
};
div().map(|element| {
v_flex().gap_1().mb_3().map(|element| {
if !edit_tools {
element.child(
v_flex()
.my_2()
.child(
h_flex()
.group("disclosure-header")
@@ -2494,7 +2680,7 @@ impl ActiveThread {
.color(Color::Muted),
)
.child(
h_flex().pr_8().text_ui_sm(cx).children(
h_flex().pr_8().text_size(rems(0.8125)).children(
rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)).on_url_click({let workspace = self.workspace.clone(); move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
}}))
@@ -2544,7 +2730,7 @@ impl ActiveThread {
)
} else {
v_flex()
.my_3()
.mb_2()
.rounded_lg()
.border_1()
.border_color(self.tool_card_border_color(cx))
@@ -2761,7 +2947,7 @@ impl ActiveThread {
)
})
}
})
}).into_any_element()
}
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
@@ -2771,53 +2957,116 @@ impl ActiveThread {
return div().into_any();
};
let default_user_rules_text = if project_context.default_user_rules.is_empty() {
None
} else if project_context.default_user_rules.len() == 1 {
let user_rules = &project_context.default_user_rules[0];
match user_rules.title.as_ref() {
Some(title) => Some(format!("Using \"{title}\" user rule")),
None => Some("Using user rule".into()),
}
} else {
Some(format!(
"Using {} user rules",
project_context.default_user_rules.len()
))
};
let first_default_user_rules_id = project_context
.default_user_rules
.first()
.map(|user_rules| user_rules.uuid);
let rules_files = project_context
.worktrees
.iter()
.filter_map(|worktree| worktree.rules_file.as_ref())
.collect::<Vec<_>>();
let label_text = match rules_files.as_slice() {
&[] => return div().into_any(),
&[rules_file] => {
format!("Using {:?} file", rules_file.path_in_worktree)
}
rules_files => {
format!("Using {} rules files", rules_files.len())
}
let rules_file_text = match rules_files.as_slice() {
&[] => None,
&[rules_file] => Some(format!(
"Using project {:?} file",
rules_file.path_in_worktree
)),
rules_files => Some(format!("Using {} project rules files", rules_files.len())),
};
div()
if default_user_rules_text.is_none() && rules_file_text.is_none() {
return div().into_any();
}
v_flex()
.pt_2()
.px_2p5()
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
.gap_1()
.when_some(
default_user_rules_text,
|parent, default_user_rules_text| {
parent.child(
h_flex()
.gap_1p5()
.w_full()
.child(
Icon::new(IconName::File)
.size(IconSize::XSmall)
.color(Color::Disabled),
)
.child(
Label::new(label_text)
Label::new(default_user_rules_text)
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx),
.truncate()
.buffer_font(cx)
.ml_1p5()
.mr_0p5(),
)
.child(
IconButton::new("open-prompt-library", IconName::ArrowUpRightAlt)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
// TODO: Figure out a way to pass focus handle here so we can display the `OpenPromptLibrary` keybinding
.tooltip(Tooltip::text("View User Rules"))
.on_click(move |_event, window, cx| {
window.dispatch_action(
Box::new(OpenPromptLibrary {
prompt_to_focus: first_default_user_rules_id,
}),
cx,
)
}),
),
)
.child(
IconButton::new("open-rule", IconName::ArrowUpRightAlt)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
.on_click(cx.listener(Self::handle_open_rules))
.tooltip(Tooltip::text("View Rules")),
),
},
)
.when_some(rules_file_text, |parent, rules_file_text| {
parent.child(
h_flex()
.w_full()
.child(
Icon::new(IconName::File)
.size(IconSize::XSmall)
.color(Color::Disabled),
)
.child(
Label::new(rules_file_text)
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx)
.ml_1p5()
.mr_0p5(),
)
.child(
IconButton::new("open-rule", IconName::ArrowUpRightAlt)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
.on_click(cx.listener(Self::handle_open_rules))
.tooltip(Tooltip::text("View Rules")),
),
)
})
.into_any()
}
@@ -2953,6 +3202,12 @@ impl ActiveThread {
}
}
pub enum ActiveThreadEvent {
EditingMessageTokenCountChanged,
}
impl EventEmitter<ActiveThreadEvent> for ActiveThread {}
impl Render for ActiveThread {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
@@ -3028,28 +3283,21 @@ pub(crate) fn open_context(
.start
.to_point(&snapshot);
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();
}
})
open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
}
}
AssistantContext::Excerpt(excerpt_context) => {
if let Some(project_path) = excerpt_context
.context_buffer
.buffer
.read(cx)
.project_path(cx)
{
let snapshot = excerpt_context.context_buffer.buffer.read(cx).snapshot();
let target_position = excerpt_context.range.start.to_point(&snapshot);
open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
}
}
@@ -3070,3 +3318,29 @@ pub(crate) fn open_context(
}
}
}
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,7 +1,7 @@
use crate::{Keep, KeepAll, Reject, RejectAll, Thread, ThreadEvent};
use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::HashSet;
use collections::{HashMap, HashSet};
use editor::{
Direction, Editor, EditorEvent, MultiBuffer, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
@@ -355,16 +355,24 @@ impl AgentDiff {
self.update_selection(&diff_hunks_in_ranges, window, cx);
}
let mut ranges_by_buffer = HashMap::default();
for hunk in &diff_hunks_in_ranges {
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer {
self.thread
.update(cx, |thread, cx| {
thread.reject_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
})
.detach_and_log_err(cx);
ranges_by_buffer
.entry(buffer.clone())
.or_insert_with(Vec::new)
.push(hunk.buffer_range.clone());
}
}
for (buffer, ranges) in ranges_by_buffer {
self.thread
.update(cx, |thread, cx| {
thread.reject_edits_in_ranges(buffer, ranges, cx)
})
.detach_and_log_err(cx);
}
}
fn update_selection(
@@ -914,6 +922,7 @@ mod tests {
language::init(cx);
Project::init_settings(cx);
AssistantSettings::register(cx);
prompt_store::init(cx);
thread_store::init(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
@@ -943,7 +952,8 @@ mod tests {
cx,
)
})
.await;
.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());

View File

@@ -40,7 +40,7 @@ pub use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant;
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
pub use crate::thread::{Message, Thread, ThreadEvent};
pub use crate::thread_store::ThreadStore;
pub use agent_diff::{AgentDiff, AgentDiffToolbar};

View File

@@ -9,11 +9,14 @@ use assistant_tool::{ToolSource, ToolWorkingSet};
use collections::HashMap;
use context_server::manager::ContextServerManager;
use fs::Fs;
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
use gpui::{
Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Subscription,
};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use settings::{Settings, update_settings_file};
use ui::{
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, Tooltip, prelude::*,
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState,
Switch, Tooltip, prelude::*,
};
use util::ResultExt as _;
use zed_actions::ExtensionCategoryFilter;
@@ -31,6 +34,8 @@ pub struct AssistantConfiguration {
expanded_context_server_tools: HashMap<Arc<str>, bool>,
tools: Entity<ToolWorkingSet>,
_registry_subscription: Subscription,
scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
}
impl AssistantConfiguration {
@@ -60,6 +65,9 @@ impl AssistantConfiguration {
},
);
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
fs,
focus_handle,
@@ -68,6 +76,8 @@ impl AssistantConfiguration {
expanded_context_server_tools: HashMap::default(),
tools,
_registry_subscription: registry_subscription,
scroll_handle,
scrollbar_state,
};
this.build_provider_configuration_views(window, cx);
this
@@ -109,7 +119,7 @@ pub enum AssistantConfigurationEvent {
impl EventEmitter<AssistantConfigurationEvent> for AssistantConfiguration {}
impl AssistantConfiguration {
fn render_provider_configuration(
fn render_provider_configuration_block(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut Context<Self>,
@@ -122,7 +132,11 @@ impl AssistantConfiguration {
.cloned();
v_flex()
.pt_3()
.pb_1()
.gap_1p5()
.border_t_1()
.border_color(cx.theme().colors().border.opacity(0.6))
.child(
h_flex()
.justify_between()
@@ -134,7 +148,7 @@ impl AssistantConfiguration {
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new(provider_name.clone())),
.child(Label::new(provider_name.clone()).size(LabelSize::Large)),
)
.when(provider.is_authenticated(cx), |parent| {
parent.child(
@@ -159,39 +173,54 @@ impl AssistantConfiguration {
)
}),
)
.map(|parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
None => parent.child(div().child(Label::new(format!(
"No configuration view for {provider_name}",
)))),
})
}
fn render_provider_configuration_section(
&mut self,
cx: &mut Context<Self>,
) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_4()
.flex_1()
.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_sm()
.map(|parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
None => parent.child(div().child(Label::new(format!(
"No configuration view for {provider_name}",
)))),
}),
v_flex()
.gap_0p5()
.child(Headline::new("LLM Providers"))
.child(
Label::new("Add at least one provider to use AI-powered features.")
.color(Color::Muted),
),
)
.children(
providers
.into_iter()
.map(|provider| self.render_provider_configuration_block(&provider, cx)),
)
}
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let always_allow_tool_actions = AssistantSettings::get_global(cx).always_allow_tool_actions;
const HEADING: &str = "Allow running tools without asking for confirmation";
const HEADING: &str = "Allow running editing tools without asking for confirmation";
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.flex_1()
.child(Headline::new("General Settings").size(HeadlineSize::Small))
.child(Headline::new("General Settings"))
.child(
h_flex()
.p_2p5()
.rounded_sm()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border)
.gap_4()
.justify_between()
.flex_wrap()
@@ -233,15 +262,13 @@ impl AssistantConfiguration {
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.flex_1()
.child(
v_flex()
.gap_0p5()
.child(
Headline::new("Model Context Protocol (MCP) Servers")
.size(HeadlineSize::Small),
)
.child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(Label::new(SUBHEADING).color(Color::Muted)),
)
.children(context_servers.into_iter().map(|context_server| {
@@ -262,9 +289,9 @@ impl AssistantConfiguration {
v_flex()
.id(SharedString::from(context_server.id()))
.border_1()
.rounded_sm()
.rounded_md()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.bg(cx.theme().colors().background.opacity(0.25))
.child(
h_flex()
.p_1()
@@ -347,34 +374,28 @@ impl AssistantConfiguration {
return parent;
}
parent.child(v_flex().children(tools.into_iter().enumerate().map(
|(ix, tool)| {
parent.child(v_flex().py_1p5().px_1().gap_1().children(
tools.into_iter().enumerate().map(|(ix, tool)| {
h_flex()
.id("tool-item")
.pl_2()
.pr_1()
.py_1()
.id(("tool-item", ix))
.px_1()
.gap_2()
.justify_between()
.when(ix < tool_count - 1, |element| {
element
.border_b_1()
.border_color(cx.theme().colors().border_variant)
})
.hover(|style| style.bg(cx.theme().colors().element_hover))
.rounded_sm()
.child(
Label::new(tool.name())
.buffer_font(cx)
.size(LabelSize::Small),
)
.child(
IconButton::new(("tool-description", ix), IconName::Info)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Ignored)
.tooltip(Tooltip::text(tool.description())),
Icon::new(IconName::Info)
.size(IconSize::Small)
.color(Color::Ignored),
)
},
)))
.tooltip(Tooltip::text(tool.description()))
}),
))
})
}))
.child(
@@ -383,7 +404,7 @@ impl AssistantConfiguration {
.gap_2()
.child(
h_flex().w_full().child(
Button::new("add-context-server", "Add MCPs Directly")
Button::new("add-context-server", "Add Custom Server")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.full_width()
@@ -426,39 +447,51 @@ impl AssistantConfiguration {
impl Render for AssistantConfiguration {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
v_flex()
.id("assistant-configuration")
.key_context("AgentConfiguration")
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().panel_background)
.relative()
.size_full()
.overflow_y_scroll()
.child(self.render_command_permission(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_context_servers_section(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.pb_8()
.bg(cx.theme().colors().panel_background)
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.mt_1()
.gap_6()
.flex_1()
.child(
v_flex()
.gap_0p5()
.child(Headline::new("LLM Providers").size(HeadlineSize::Small))
.child(
Label::new("Add at least one provider to use AI-powered features.")
.color(Color::Muted),
),
)
.children(
providers
.into_iter()
.map(|provider| self.render_provider_configuration(&provider, cx)),
),
.id("assistant-configuration-content")
.track_scroll(&self.scroll_handle)
.size_full()
.overflow_y_scroll()
.child(self.render_command_permission(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_context_servers_section(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.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())),
)
}
}

View File

@@ -2,7 +2,7 @@ use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
use serde_json::json;
use settings::update_settings_file;
use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui_input::SingleLineInput;
use workspace::{ModalView, Workspace};
@@ -34,9 +34,9 @@ impl AddContextServerModal {
cx: &mut Context<Self>,
) -> Self {
let name_editor =
cx.new(|cx| SingleLineInput::new(window, cx, "Your server name").label("Name"));
cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
let command_editor = cx.new(|cx| {
SingleLineInput::new(window, cx, "Command").label("Command to run the context server")
SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
});
Self {
@@ -46,7 +46,7 @@ impl AddContextServerModal {
}
}
fn confirm(&mut self, cx: &mut Context<Self>) {
fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
let name = self
.name_editor
.read(cx)
@@ -96,7 +96,7 @@ impl AddContextServerModal {
cx.emit(DismissEvent);
}
fn cancel(&mut self, cx: &mut Context<Self>) {
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
@@ -112,38 +112,68 @@ impl Focusable for AddContextServerModal {
impl EventEmitter<DismissEvent> for AddContextServerModal {}
impl Render for AddContextServerModal {
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 {
let is_name_empty = self.name_editor.read(cx).is_empty(cx);
let is_command_empty = self.command_editor.read(cx).is_empty(cx);
let focus_handle = self.focus_handle(cx);
div()
.elevation_3(cx)
.w(rems(34.))
.key_context("AddContextServerModal")
.on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(cx)))
.on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
.on_action(
cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
)
.on_action(
cx.listener(|this, _: &menu::Confirm, _window, cx| {
this.confirm(&menu::Confirm, cx)
}),
)
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(
Modal::new("add-context-server", None)
.header(ModalHeader::new().headline("Add Context Server"))
.header(ModalHeader::new().headline("Add MCP Server"))
.section(
Section::new()
.child(self.name_editor.clone())
.child(self.command_editor.clone()),
Section::new().child(
v_flex()
.gap_2()
.child(self.name_editor.clone())
.child(self.command_editor.clone()),
),
)
.footer(
ModalFooter::new()
.start_slot(
Button::new("cancel", "Cancel").on_click(
cx.listener(|this, _event, _window, cx| this.cancel(cx)),
),
Button::new("cancel", "Cancel")
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(cx.listener(|this, _event, _window, cx| {
this.cancel(&menu::Cancel, cx)
})),
)
.end_slot(
Button::new("add-server", "Add Server")
.disabled(is_name_empty || is_command_empty)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.map(|button| {
if is_name_empty {
button.tooltip(Tooltip::text("Name is required"))
@@ -153,9 +183,9 @@ impl Render for AddContextServerModal {
button
}
})
.on_click(
cx.listener(|this, _event, _window, cx| this.confirm(cx)),
),
.on_click(cx.listener(|this, _event, _window, cx| {
this.confirm(&menu::Confirm, cx)
})),
),
),
)

View File

@@ -1,3 +1,4 @@
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -5,14 +6,14 @@ use std::time::Duration;
use anyhow::{Result, anyhow};
use assistant_context_editor::{
AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
make_lsp_adapter_delegate, render_remaining_tokens,
humanize_token_count, make_lsp_adapter_delegate, render_remaining_tokens,
};
use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use editor::{Editor, EditorEvent, MultiBuffer};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
@@ -24,7 +25,8 @@ use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector;
use project::Project;
use prompt_library::{PromptLibrary, open_prompt_library};
use prompt_store::PromptBuilder;
use prompt_store::{PromptBuilder, PromptId};
use proto::Plan;
use settings::{Settings, update_settings_file};
use time::UtcOffset;
use ui::{
@@ -36,15 +38,16 @@ use workspace::dock::{DockPosition, Panel, PanelEvent};
use zed_actions::agent::OpenConfiguration;
use zed_actions::assistant::{OpenPromptLibrary, ToggleFocus};
use crate::active_thread::ActiveThread;
use crate::active_thread::{ActiveThread, ActiveThreadEvent};
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
use crate::history_store::{HistoryEntry, HistoryStore};
use crate::message_editor::MessageEditor;
use crate::message_editor::{MessageEditor, MessageEditorEvent};
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::ui::UsageBanner;
use crate::{
AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
AddContextServer, AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
};
@@ -80,7 +83,7 @@ pub fn init(cx: &mut App) {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
panel.deploy_prompt_library(&OpenPromptLibrary::default(), window, cx)
});
}
})
@@ -111,7 +114,9 @@ enum ActiveView {
change_title_editor: Entity<Editor>,
_subscriptions: Vec<gpui::Subscription>,
},
PromptEditor,
PromptEditor {
context_editor: Entity<ContextEditor>,
},
History,
Configuration,
}
@@ -180,10 +185,9 @@ pub struct AssistantPanel {
language_registry: Arc<LanguageRegistry>,
thread_store: Entity<ThreadStore>,
thread: Entity<ActiveThread>,
_thread_subscription: Subscription,
message_editor: Entity<MessageEditor>,
_active_thread_subscriptions: Vec<Subscription>,
context_store: Entity<assistant_context_editor::ContextStore>,
context_editor: Option<Entity<ContextEditor>>,
configuration: Option<Entity<AssistantConfiguration>>,
configuration_subscription: Option<Subscription>,
local_timezone: UtcOffset,
@@ -209,7 +213,7 @@ impl AssistantPanel {
let project = workspace.project().clone();
ThreadStore::load(project, tools.clone(), prompt_builder.clone(), cx)
})?
.await;
.await?;
let slash_commands = Arc::new(SlashCommandWorkingSet::default());
let context_store = workspace
@@ -263,6 +267,13 @@ impl AssistantPanel {
)
});
let message_editor_subscription =
cx.subscribe(&message_editor, |_, _, event, cx| match event {
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
cx.notify();
}
});
let history_store =
cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
@@ -287,6 +298,12 @@ impl AssistantPanel {
)
});
let active_thread_subscription = cx.subscribe(&thread, |_, _, event, cx| match &event {
ActiveThreadEvent::EditingMessageTokenCountChanged => {
cx.notify();
}
});
Self {
active_view,
workspace,
@@ -295,10 +312,13 @@ impl AssistantPanel {
language_registry,
thread_store: thread_store.clone(),
thread,
_thread_subscription: thread_subscription,
message_editor,
_active_thread_subscriptions: vec![
thread_subscription,
active_thread_subscription,
message_editor_subscription,
],
context_store,
context_editor: None,
configuration: None,
configuration_subscription: None,
local_timezone: UtcOffset::from_whole_seconds(
@@ -381,6 +401,13 @@ impl AssistantPanel {
.detach_and_log_err(cx);
}
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
if let ThreadEvent::MessageAdded(_) = &event {
// needed to leave empty state
cx.notify();
}
});
self.thread = cx.new(|cx| {
ActiveThread::new(
thread.clone(),
@@ -393,12 +420,12 @@ impl AssistantPanel {
)
});
self._thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
if let ThreadEvent::MessageAdded(_) = &event {
// needed to leave empty state
cx.notify();
}
});
let active_thread_subscription =
cx.subscribe(&self.thread, |_, _, event, cx| match &event {
ActiveThreadEvent::EditingMessageTokenCountChanged => {
cx.notify();
}
});
self.message_editor = cx.new(|cx| {
MessageEditor::new(
@@ -412,11 +439,22 @@ impl AssistantPanel {
)
});
self.message_editor.focus_handle(cx).focus(window);
let message_editor_subscription =
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
cx.notify();
}
});
self._active_thread_subscriptions = vec![
thread_subscription,
active_thread_subscription,
message_editor_subscription,
];
}
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.set_active_view(ActiveView::PromptEditor, window, cx);
let context = self
.context_store
.update(cx, |context_store, cx| context_store.create(cx));
@@ -424,7 +462,7 @@ impl AssistantPanel {
.log_err()
.flatten();
self.context_editor = Some(cx.new(|cx| {
let context_editor = cx.new(|cx| {
let mut editor = ContextEditor::for_context(
context,
self.fs.clone(),
@@ -436,16 +474,21 @@ impl AssistantPanel {
);
editor.insert_default_prompt(window, cx);
editor
}));
});
if let Some(context_editor) = self.context_editor.as_ref() {
context_editor.focus_handle(cx).focus(window);
}
self.set_active_view(
ActiveView::PromptEditor {
context_editor: context_editor.clone(),
},
window,
cx,
);
context_editor.focus_handle(cx).focus(window);
}
fn deploy_prompt_library(
&mut self,
_: &OpenPromptLibrary,
action: &OpenPromptLibrary,
_window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -459,6 +502,7 @@ impl AssistantPanel {
None,
))
}),
action.prompt_to_focus.map(|uuid| PromptId::User { uuid }),
cx,
)
.detach_and_log_err(cx);
@@ -507,8 +551,13 @@ impl AssistantPanel {
cx,
)
});
this.set_active_view(ActiveView::PromptEditor, window, cx);
this.context_editor = Some(editor);
this.set_active_view(
ActiveView::PromptEditor {
context_editor: editor,
},
window,
cx,
);
anyhow::Ok(())
})??;
@@ -537,6 +586,13 @@ impl AssistantPanel {
Some(this.thread_store.downgrade()),
)
});
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
if let ThreadEvent::MessageAdded(_) = &event {
// needed to leave empty state
cx.notify();
}
});
this.thread = cx.new(|cx| {
ActiveThread::new(
thread.clone(),
@@ -548,6 +604,14 @@ impl AssistantPanel {
cx,
)
});
let active_thread_subscription =
cx.subscribe(&this.thread, |_, _, event, cx| match &event {
ActiveThreadEvent::EditingMessageTokenCountChanged => {
cx.notify();
}
});
this.message_editor = cx.new(|cx| {
MessageEditor::new(
this.fs.clone(),
@@ -560,6 +624,19 @@ impl AssistantPanel {
)
});
this.message_editor.focus_handle(cx).focus(window);
let message_editor_subscription =
cx.subscribe(&this.message_editor, |_, _, event, cx| match event {
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
cx.notify();
}
});
this._active_thread_subscriptions = vec![
thread_subscription,
active_thread_subscription,
message_editor_subscription,
];
})
})
}
@@ -711,8 +788,15 @@ impl AssistantPanel {
.update(cx, |this, cx| this.delete_thread(thread_id, cx))
}
pub(crate) fn has_active_thread(&self) -> bool {
matches!(self.active_view, ActiveView::Thread { .. })
}
pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
self.context_editor.clone()
match &self.active_view {
ActiveView::PromptEditor { context_editor } => Some(context_editor.clone()),
_ => None,
}
}
pub(crate) fn delete_context(
@@ -750,16 +834,10 @@ impl AssistantPanel {
impl Focusable for AssistantPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match self.active_view {
match &self.active_view {
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
ActiveView::History => self.history.focus_handle(cx),
ActiveView::PromptEditor => {
if let Some(context_editor) = self.context_editor.as_ref() {
context_editor.focus_handle(cx)
} else {
cx.focus_handle()
}
}
ActiveView::PromptEditor { context_editor } => context_editor.focus_handle(cx),
ActiveView::Configuration => {
if let Some(configuration) = self.configuration.as_ref() {
configuration.focus_handle(cx)
@@ -852,7 +930,7 @@ impl Panel for AssistantPanel {
}
impl AssistantPanel {
fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
let content = match &self.active_view {
@@ -883,15 +961,8 @@ impl AssistantPanel {
.into_any_element()
}
}
ActiveView::PromptEditor => {
let title = self
.context_editor
.as_ref()
.map(|context_editor| {
SharedString::from(context_editor.read(cx).title(cx).to_string())
})
.unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
ActiveView::PromptEditor { context_editor } => {
let title = SharedString::from(context_editor.read(cx).title(cx).to_string());
Label::new(title).ml_2().truncate().into_any_element()
}
ActiveView::History => Label::new("History").truncate().into_any_element(),
@@ -912,21 +983,18 @@ impl AssistantPanel {
fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let active_thread = self.thread.read(cx);
let thread = active_thread.thread().read(cx);
let token_usage = thread.total_token_usage(cx);
let thread_id = thread.id().clone();
let is_generating = thread.is_generating();
let is_empty = active_thread.is_empty();
let focus_handle = self.focus_handle(cx);
let is_history = matches!(self.active_view, ActiveView::History);
let show_token_count = match &self.active_view {
ActiveView::Thread { .. } => !is_empty,
ActiveView::PromptEditor => self.context_editor.is_some(),
ActiveView::PromptEditor { .. } => true,
_ => false,
};
let focus_handle = self.focus_handle(cx);
let go_back_button = match &self.active_view {
ActiveView::History | ActiveView::Configuration => Some(
div().pl_1().child(
@@ -973,69 +1041,9 @@ impl AssistantPanel {
h_flex()
.h_full()
.gap_2()
.when(show_token_count, |parent| match self.active_view {
ActiveView::Thread { .. } => {
if token_usage.total == 0 {
return parent;
}
let token_color = match token_usage.ratio {
TokenUsageRatio::Normal => Color::Muted,
TokenUsageRatio::Warning => Color::Warning,
TokenUsageRatio::Exceeded => Color::Error,
};
parent.child(
h_flex()
.flex_shrink_0()
.gap_0p5()
.child(
Label::new(assistant_context_editor::humanize_token_count(
token_usage.total,
))
.size(LabelSize::Small)
.color(token_color)
.map(|label| {
if is_generating {
label
.with_animation(
"used-tokens-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(
0.6, 1.,
)),
|label, delta| label.alpha(delta),
)
.into_any()
} else {
label.into_any_element()
}
}),
)
.child(
Label::new("/").size(LabelSize::Small).color(Color::Muted),
)
.child(
Label::new(assistant_context_editor::humanize_token_count(
token_usage.max,
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
}
ActiveView::PromptEditor => {
let Some(editor) = self.context_editor.as_ref() else {
return parent;
};
let Some(element) = render_remaining_tokens(editor, cx) else {
return parent;
};
parent.child(element)
}
_ => parent,
})
.when(show_token_count, |parent|
parent.children(self.render_token_count(&thread, cx))
)
.child(
h_flex()
.h_full()
@@ -1112,17 +1120,19 @@ impl AssistantPanel {
"New Text Thread",
NewTextThread.boxed_clone(),
)
.action("Settings", OpenConfiguration.boxed_clone())
.action("Prompt Library", Box::new(OpenPromptLibrary::default()))
.action("Settings", Box::new(OpenConfiguration))
.separator()
.header("MCPs")
.action(
"Install MCPs",
zed_actions::Extensions {
"View Server Extensions",
Box::new(zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::ContextServers,
),
}
.boxed_clone(),
}),
)
.action("Add Custom Server", Box::new(AddContextServer))
},
))
}),
@@ -1131,6 +1141,110 @@ impl AssistantPanel {
)
}
fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
let is_generating = thread.is_generating();
let message_editor = self.message_editor.read(cx);
let conversation_token_usage = thread.total_token_usage(cx);
let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
self.thread.read(cx).editing_message_id()
{
let combined = thread
.token_usage_up_to_message(editing_message_id, cx)
.add(unsent_tokens);
(combined, unsent_tokens > 0)
} else {
let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
let combined = conversation_token_usage.add(unsent_tokens);
(combined, unsent_tokens > 0)
};
let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
match &self.active_view {
ActiveView::Thread { .. } => {
if total_token_usage.total == 0 {
return None;
}
let token_color = match total_token_usage.ratio() {
TokenUsageRatio::Normal if is_estimating => Color::Default,
TokenUsageRatio::Normal => Color::Muted,
TokenUsageRatio::Warning => Color::Warning,
TokenUsageRatio::Exceeded => Color::Error,
};
let token_count = h_flex()
.id("token-count")
.flex_shrink_0()
.gap_0p5()
.when(!is_generating && is_estimating, |parent| {
parent
.child(
h_flex()
.mr_1()
.size_2p5()
.justify_center()
.rounded_full()
.bg(cx.theme().colors().text.opacity(0.1))
.child(
div().size_1().rounded_full().bg(cx.theme().colors().text),
),
)
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Estimated New Token Count",
None,
format!(
"Current Conversation Tokens: {}",
humanize_token_count(conversation_token_usage.total)
),
window,
cx,
)
})
})
.child(
Label::new(humanize_token_count(total_token_usage.total))
.size(LabelSize::Small)
.color(token_color)
.map(|label| {
if is_generating || is_waiting_to_update_token_count {
label
.with_animation(
"used-tokens-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
)
.into_any()
} else {
label.into_any_element()
}
}),
)
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
.child(
Label::new(humanize_token_count(total_token_usage.max))
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any();
Some(token_count)
}
ActiveView::PromptEditor { context_editor } => {
let element = render_remaining_tokens(context_editor, cx)?;
Some(element.into_any_element())
}
_ => None,
}
}
fn render_active_thread_or_empty_state(
&self,
window: &mut Window,
@@ -1431,6 +1545,12 @@ impl AssistantPanel {
})
}
fn render_usage_banner(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let usage = self.thread.read(cx).last_usage()?;
Some(UsageBanner::new(zed_llm_client::Plan::ZedProTrial, usage.amount).into_any_element())
}
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let last_error = self.thread.read(cx).last_error()?;
@@ -1449,6 +1569,9 @@ impl AssistantPanel {
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
ThreadError::ModelRequestLimitReached { plan } => {
self.render_model_request_limit_reached_error(plan, cx)
}
ThreadError::Message { header, message } => {
self.render_error_message(header, message, cx)
}
@@ -1551,6 +1674,71 @@ impl AssistantPanel {
.into_any()
}
fn render_model_request_limit_reached_error(
&self,
plan: Plan,
cx: &mut Context<Self>,
) -> AnyElement {
let error_message = match plan {
Plan::ZedPro => {
"Model request limit reached. Upgrade to usage-based billing for more requests."
}
Plan::ZedProTrial => {
"Model request limit reached. Upgrade to Zed Pro for more requests."
}
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
};
let call_to_action = match plan {
Plan::ZedPro => "Upgrade to usage-based billing",
Plan::ZedProTrial => "Upgrade to Zed Pro",
Plan::Free => "Upgrade to Zed Pro",
};
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(error_message)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(
Button::new("subscribe", call_to_action).on_click(cx.listener(
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
},
)),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
fn render_error_message(
&self,
header: SharedString,
@@ -1593,7 +1781,7 @@ impl AssistantPanel {
fn key_context(&self) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("AgentPanel");
if matches!(self.active_view, ActiveView::PromptEditor) {
if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
key_context.add("prompt_editor");
}
key_context
@@ -1621,13 +1809,14 @@ impl Render for AssistantPanel {
.on_action(cx.listener(Self::open_agent_diff))
.on_action(cx.listener(Self::go_back))
.child(self.render_toolbar(window, cx))
.map(|parent| match self.active_view {
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent
.child(self.render_active_thread_or_empty_state(window, cx))
.children(self.render_usage_banner(cx))
.child(h_flex().child(self.message_editor.clone()))
.children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()),
ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
ActiveView::PromptEditor { context_editor } => parent.child(context_editor.clone()),
ActiveView::Configuration => parent.children(self.configuration.clone()),
})
}
@@ -1692,7 +1881,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
cx: &mut Context<Workspace>,
) -> Option<Entity<ContextEditor>> {
let panel = workspace.panel::<AssistantPanel>(cx)?;
panel.update(cx, |panel, _cx| panel.context_editor.clone())
panel.read(cx).active_context_editor()
}
fn open_saved_context(
@@ -1723,10 +1912,59 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn quote_selection(
&self,
_workspace: &mut Workspace,
_creases: Vec<(String, String)>,
_window: &mut Window,
_cx: &mut Context<Workspace>,
workspace: &mut Workspace,
selection_ranges: Vec<Range<Anchor>>,
buffer: Entity<MultiBuffer>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
if !panel.focus_handle(cx).contains_focused(window, cx) {
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
}
panel.update(cx, |_, cx| {
// Wait to create a new context until the workspace is no longer
// being updated.
cx.defer_in(window, move |panel, window, cx| {
if panel.has_active_thread() {
panel.thread.update(cx, |thread, cx| {
thread.context_store().update(cx, |store, cx| {
let buffer = buffer.read(cx);
let selection_ranges = selection_ranges
.into_iter()
.flat_map(|range| {
let (start_buffer, start) =
buffer.text_anchor_for_position(range.start, cx)?;
let (end_buffer, end) =
buffer.text_anchor_for_position(range.end, cx)?;
if start_buffer != end_buffer {
return None;
}
Some((start_buffer, start..end))
})
.collect::<Vec<_>>();
for (buffer, range) in selection_ranges {
store.add_excerpt(range, buffer, cx).detach_and_log_err(cx);
}
})
})
} else if let Some(context_editor) = panel.active_context_editor() {
let snapshot = buffer.read(cx).snapshot(cx);
let selection_ranges = selection_ranges
.into_iter()
.map(|range| range.to_point(&snapshot))
.collect::<Vec<_>>();
context_editor.update(cx, |context_editor, cx| {
context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
});
}
});
});
}
}

View File

@@ -425,6 +425,8 @@ impl CodegenAlternative {
request_message.content.push(prompt.into());
Ok(LanguageModelRequest {
thread_id: None,
prompt_id: None,
tools: Vec::new(),
stop: Vec::new(),
temperature: None,

View File

@@ -4,6 +4,7 @@ use gpui::{App, Entity, SharedString};
use language::{Buffer, File};
use language_model::LanguageModelRequestMessage;
use project::{ProjectPath, Worktree};
use rope::Point;
use serde::{Deserialize, Serialize};
use text::{Anchor, BufferId};
use ui::IconName;
@@ -23,6 +24,7 @@ pub enum ContextKind {
File,
Directory,
Symbol,
Excerpt,
FetchedUrl,
Thread,
}
@@ -33,6 +35,7 @@ impl ContextKind {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::Symbol => IconName::Code,
ContextKind::Excerpt => IconName::Code,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageBubbles,
}
@@ -46,6 +49,7 @@ pub enum AssistantContext {
Symbol(SymbolContext),
FetchedUrl(FetchedUrlContext),
Thread(ThreadContext),
Excerpt(ExcerptContext),
}
impl AssistantContext {
@@ -56,6 +60,7 @@ impl AssistantContext {
Self::Symbol(symbol) => symbol.id,
Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id,
Self::Excerpt(excerpt) => excerpt.id,
}
}
}
@@ -155,6 +160,14 @@ pub struct ContextSymbolId {
pub range: Range<Anchor>,
}
#[derive(Debug, Clone)]
pub struct ExcerptContext {
pub id: ContextId,
pub range: Range<Anchor>,
pub line_range: Range<Point>,
pub context_buffer: ContextBuffer,
}
/// Formats a collection of contexts into a string representation
pub fn format_context_as_string<'a>(
contexts: impl Iterator<Item = &'a AssistantContext>,
@@ -163,6 +176,7 @@ pub fn format_context_as_string<'a>(
let mut file_context = Vec::new();
let mut directory_context = Vec::new();
let mut symbol_context = Vec::new();
let mut excerpt_context = Vec::new();
let mut fetch_context = Vec::new();
let mut thread_context = Vec::new();
@@ -171,6 +185,7 @@ pub fn format_context_as_string<'a>(
AssistantContext::File(context) => file_context.push(context),
AssistantContext::Directory(context) => directory_context.push(context),
AssistantContext::Symbol(context) => symbol_context.push(context),
AssistantContext::Excerpt(context) => excerpt_context.push(context),
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
AssistantContext::Thread(context) => thread_context.push(context),
}
@@ -179,6 +194,7 @@ pub fn format_context_as_string<'a>(
if file_context.is_empty()
&& directory_context.is_empty()
&& symbol_context.is_empty()
&& excerpt_context.is_empty()
&& fetch_context.is_empty()
&& thread_context.is_empty()
{
@@ -216,6 +232,15 @@ pub fn format_context_as_string<'a>(
result.push_str("</symbols>\n");
}
if !excerpt_context.is_empty() {
result.push_str("<excerpts>\n");
for context in excerpt_context {
result.push_str(&context.context_buffer.text);
result.push('\n');
}
result.push_str("</excerpts>\n");
}
if !fetch_context.is_empty() {
result.push_str("<fetched_urls>\n");
for context in &fetch_context {

View File

@@ -8,6 +8,7 @@ use std::sync::atomic::AtomicBool;
use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId};
use file_icons::FileIcons;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use http_client::HttpClientWithUrl;
use language::{Buffer, CodeLabel, HighlightId};
@@ -37,7 +38,24 @@ pub(crate) enum Match {
File(FileMatch),
Thread(ThreadMatch),
Fetch(SharedString),
Mode(ContextPickerMode),
Mode(ModeMatch),
}
pub struct ModeMatch {
mat: Option<StringMatch>,
mode: ContextPickerMode,
}
impl Match {
pub fn score(&self) -> f64 {
match self {
Match::File(file) => file.mat.score,
Match::Mode(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
Match::Thread(_) => 1.,
Match::Symbol(_) => 1.,
Match::Fetch(_) => 1.,
}
}
}
fn search(
@@ -126,19 +144,54 @@ fn search(
matches.extend(
supported_context_picker_modes(&thread_store)
.into_iter()
.map(Match::Mode),
.map(|mode| Match::Mode(ModeMatch { mode, mat: None })),
);
Task::ready(matches)
} else {
let executor = cx.background_executor().clone();
let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
let modes = supported_context_picker_modes(&thread_store);
let mode_candidates = modes
.iter()
.enumerate()
.map(|(ix, mode)| StringMatchCandidate::new(ix, mode.mention_prefix()))
.collect::<Vec<_>>();
cx.background_spawn(async move {
search_files_task
let mut matches = search_files_task
.await
.into_iter()
.map(Match::File)
.collect()
.collect::<Vec<_>>();
let mode_matches = fuzzy::match_strings(
&mode_candidates,
&query,
false,
100,
&Arc::new(AtomicBool::default()),
executor,
)
.await;
matches.extend(mode_matches.into_iter().map(|mat| {
Match::Mode(ModeMatch {
mode: modes[mat.candidate_id],
mat: Some(mat),
})
}));
matches.sort_by(|a, b| {
b.score()
.partial_cmp(&a.score())
.unwrap_or(std::cmp::Ordering::Equal)
});
matches
})
}
}
@@ -548,7 +601,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
context_store.clone(),
http_client.clone(),
)),
Match::Mode(mode) => {
Match::Mode(ModeMatch { mode, .. }) => {
Some(Self::completion_for_mode(source_range.clone(), mode))
}
})

View File

@@ -9,14 +9,14 @@ use futures::{self, Future, FutureExt, future};
use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
use language::{Buffer, File};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use rope::Rope;
use rope::{Point, Rope};
use text::{Anchor, BufferId, OffsetRangeExt};
use util::{ResultExt as _, maybe};
use crate::ThreadStore;
use crate::context::{
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
ExcerptContext, FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
};
use crate::context_strip::SuggestedContext;
use crate::thread::{Thread, ThreadId};
@@ -110,7 +110,7 @@ impl ContextStore {
}
let (buffer_info, text_task) =
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
let text = text_task.await;
@@ -129,7 +129,7 @@ impl ContextStore {
) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
let (buffer_info, text_task) =
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
let text = text_task.await;
@@ -206,7 +206,7 @@ impl ContextStore {
// Skip all binary files and other non-UTF8 files
for buffer in buffers.into_iter().flatten() {
if let Some((buffer_info, text_task)) =
collect_buffer_info_and_text(buffer, None, cx).log_err()
collect_buffer_info_and_text(buffer, cx).log_err()
{
buffer_infos.push(buffer_info);
text_tasks.push(text_task);
@@ -290,11 +290,14 @@ impl ContextStore {
}
}
let (buffer_info, collect_content_task) =
match collect_buffer_info_and_text(buffer, Some(symbol_enclosing_range.clone()), cx) {
Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
Err(err) => return Task::ready(Err(err)),
};
let (buffer_info, collect_content_task) = match collect_buffer_info_and_text_for_range(
buffer,
symbol_enclosing_range.clone(),
cx,
) {
Ok((_, buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
Err(err) => return Task::ready(Err(err)),
};
cx.spawn(async move |this, cx| {
let content = collect_content_task.await;
@@ -416,6 +419,49 @@ impl ContextStore {
cx.notify();
}
pub fn add_excerpt(
&mut self,
range: Range<Anchor>,
buffer: Entity<Buffer>,
cx: &mut Context<ContextStore>,
) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
let (line_range, buffer_info, text_task) = this.update(cx, |_, cx| {
collect_buffer_info_and_text_for_range(buffer, range.clone(), cx)
})??;
let text = text_task.await;
this.update(cx, |this, cx| {
this.insert_excerpt(
make_context_buffer(buffer_info, text),
range,
line_range,
cx,
)
})?;
anyhow::Ok(())
})
}
fn insert_excerpt(
&mut self,
context_buffer: ContextBuffer,
range: Range<Anchor>,
line_range: Range<Point>,
cx: &mut Context<Self>,
) {
let id = self.next_context_id.post_inc();
self.context.push(AssistantContext::Excerpt(ExcerptContext {
id,
range,
line_range,
context_buffer,
}));
cx.notify();
}
pub fn accept_suggested_context(
&mut self,
suggested: &SuggestedContext,
@@ -465,6 +511,7 @@ impl ContextStore {
self.symbol_buffers.remove(&symbol.context_symbol.id);
self.symbols.retain(|_, context_id| *context_id != id);
}
AssistantContext::Excerpt(_) => {}
AssistantContext::FetchedUrl(_) => {
self.fetched_urls.retain(|_, context_id| *context_id != id);
}
@@ -592,6 +639,7 @@ impl ContextStore {
}
AssistantContext::Directory(_)
| AssistantContext::Symbol(_)
| AssistantContext::Excerpt(_)
| AssistantContext::FetchedUrl(_)
| AssistantContext::Thread(_) => None,
})
@@ -643,41 +691,78 @@ fn make_context_symbol(
}
}
fn collect_buffer_info_and_text_for_range(
buffer: Entity<Buffer>,
range: Range<Anchor>,
cx: &App,
) -> Result<(Range<Point>, BufferInfo, Task<SharedString>)> {
let content = buffer
.read(cx)
.text_for_range(range.clone())
.collect::<Rope>();
let line_range = range.to_point(&buffer.read(cx).snapshot());
let buffer_info = collect_buffer_info(buffer, cx)?;
let full_path = buffer_info.file.full_path(cx);
let text_task = cx.background_spawn({
let line_range = line_range.clone();
async move { to_fenced_codeblock(&full_path, content, Some(line_range)) }
});
Ok((line_range, buffer_info, text_task))
}
fn collect_buffer_info_and_text(
buffer: Entity<Buffer>,
range: Option<Range<Anchor>>,
cx: &App,
) -> Result<(BufferInfo, Task<SharedString>)> {
let content = buffer.read(cx).as_rope().clone();
let buffer_info = collect_buffer_info(buffer, cx)?;
let full_path = buffer_info.file.full_path(cx);
let text_task =
cx.background_spawn(async move { to_fenced_codeblock(&full_path, content, None) });
Ok((buffer_info, text_task))
}
fn collect_buffer_info(buffer: Entity<Buffer>, cx: &App) -> Result<BufferInfo> {
let buffer_ref = buffer.read(cx);
let file = buffer_ref.file().context("file context must have a path")?;
// Important to collect version at the same time as content so that staleness logic is correct.
let version = buffer_ref.version();
let content = if let Some(range) = range {
buffer_ref.text_for_range(range).collect::<Rope>()
} else {
buffer_ref.as_rope().clone()
};
let buffer_info = BufferInfo {
Ok(BufferInfo {
buffer,
id: buffer_ref.remote_id(),
file: file.clone(),
version,
};
let full_path = file.full_path(cx);
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&full_path, content) });
Ok((buffer_info, text_task))
})
}
fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
fn to_fenced_codeblock(
path: &Path,
content: Rope,
line_range: Option<Range<Point>>,
) -> SharedString {
let line_range_text = line_range.map(|range| {
if range.start.row == range.end.row {
format!(":{}", range.start.row + 1)
} else {
format!(":{}-{}", range.start.row + 1, range.end.row + 1)
}
});
let path_extension = path.extension().and_then(|ext| ext.to_str());
let path_string = path.to_string_lossy();
let capacity = 3
+ path_extension.map_or(0, |extension| extension.len() + 1)
+ path_string.len()
+ line_range_text.as_ref().map_or(0, |text| text.len())
+ 1
+ content.len()
+ 5;
@@ -691,6 +776,10 @@ fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
}
buffer.push_str(&path_string);
if let Some(line_range_text) = line_range_text {
buffer.push_str(&line_range_text);
}
buffer.push('\n');
for chunk in content.chunks() {
buffer.push_str(&chunk);
@@ -769,6 +858,14 @@ pub fn refresh_context_store_text(
return refresh_symbol_text(context_store, symbol_context, cx);
}
}
AssistantContext::Excerpt(excerpt_context) => {
if changed_buffers.is_empty()
|| changed_buffers.contains(&excerpt_context.context_buffer.buffer)
{
let context_store = context_store.clone();
return refresh_excerpt_text(context_store, excerpt_context, cx);
}
}
AssistantContext::Thread(thread_context) => {
if changed_buffers.is_empty() {
let context_store = context_store.clone();
@@ -880,6 +977,34 @@ fn refresh_symbol_text(
}
}
fn refresh_excerpt_text(
context_store: Entity<ContextStore>,
excerpt_context: &ExcerptContext,
cx: &App,
) -> Option<Task<()>> {
let id = excerpt_context.id;
let range = excerpt_context.range.clone();
let task = refresh_context_excerpt(&excerpt_context.context_buffer, range.clone(), cx);
if let Some(task) = task {
Some(cx.spawn(async move |cx| {
let (line_range, context_buffer) = task.await;
context_store
.update(cx, |context_store, _| {
let new_excerpt_context = ExcerptContext {
id,
range,
line_range,
context_buffer,
};
context_store.replace_context(AssistantContext::Excerpt(new_excerpt_context));
})
.ok();
}))
} else {
None
}
}
fn refresh_thread_text(
context_store: Entity<ContextStore>,
thread_context: &ThreadContext,
@@ -908,13 +1033,29 @@ fn refresh_context_buffer(
let buffer = context_buffer.buffer.read(cx);
if buffer.version.changed_since(&context_buffer.version) {
let (buffer_info, text_task) =
collect_buffer_info_and_text(context_buffer.buffer.clone(), None, cx).log_err()?;
collect_buffer_info_and_text(context_buffer.buffer.clone(), cx).log_err()?;
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
} else {
None
}
}
fn refresh_context_excerpt(
context_buffer: &ContextBuffer,
range: Range<Anchor>,
cx: &App,
) -> Option<impl Future<Output = (Range<Point>, ContextBuffer)> + use<>> {
let buffer = context_buffer.buffer.read(cx);
if buffer.version.changed_since(&context_buffer.version) {
let (line_range, buffer_info, text_task) =
collect_buffer_info_and_text_for_range(context_buffer.buffer.clone(), range, cx)
.log_err()?;
Some(text_task.map(move |text| (line_range, make_context_buffer(buffer_info, text))))
} else {
None
}
}
fn refresh_context_symbol(
context_symbol: &ContextSymbol,
cx: &App,
@@ -922,9 +1063,9 @@ fn refresh_context_symbol(
let buffer = context_symbol.buffer.read(cx);
let project_path = buffer.project_path(cx)?;
if buffer.version.changed_since(&context_symbol.buffer_version) {
let (buffer_info, text_task) = collect_buffer_info_and_text(
let (_, buffer_info, text_task) = collect_buffer_info_and_text_for_range(
context_symbol.buffer.clone(),
Some(context_symbol.enclosing_range.clone()),
context_symbol.enclosing_range.clone(),
cx,
)
.log_err()?;

View File

@@ -2,22 +2,23 @@ use std::collections::BTreeMap;
use std::sync::Arc;
use crate::assistant_model_selector::ModelType;
use crate::context::{AssistantContext, format_context_as_string};
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
use buffer_diff::BufferDiff;
use collections::HashSet;
use editor::actions::MoveUp;
use editor::{
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, EditorStyle,
MultiBuffer,
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode,
EditorStyle, MultiBuffer,
};
use file_icons::FileIcons;
use fs::Fs;
use gpui::{
Animation, AnimationExt, App, Entity, Focusable, Subscription, TextStyle, WeakEntity,
linear_color_stop, linear_gradient, point, pulsating_between,
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
};
use language::{Buffer, Language};
use language_model::{ConfiguredModel, LanguageModelRegistry};
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage};
use language_model_selector::ToggleModelSelector;
use multi_buffer;
use project::Project;
@@ -33,7 +34,7 @@ use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
use crate::context_store::{ContextStore, refresh_context_store_text};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::thread::{RequestKind, Thread, TokenUsageRatio};
use crate::thread::{Thread, TokenUsageRatio};
use crate::thread_store::ThreadStore;
use crate::{
AgentDiff, Chat, ChatMode, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,
@@ -55,6 +56,8 @@ pub struct MessageEditor {
edits_expanded: bool,
editor_is_expanded: bool,
waiting_for_summaries_to_send: bool,
last_estimated_token_count: Option<usize>,
update_token_count_task: Option<Task<anyhow::Result<()>>>,
_subscriptions: Vec<Subscription>,
}
@@ -129,8 +132,18 @@ impl MessageEditor {
let incompatible_tools =
cx.new(|cx| IncompatibleToolsState::new(thread.read(cx).tools().clone(), cx));
let subscriptions =
vec![cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event)];
let subscriptions = vec![
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
cx.subscribe(&editor, |this, _, event, cx| match event {
EditorEvent::BufferEdited => {
this.message_or_context_changed(true, cx);
}
_ => {}
}),
cx.observe(&context_store, |this, _, cx| {
this.message_or_context_changed(false, cx);
}),
];
Self {
editor: editor.clone(),
@@ -156,6 +169,8 @@ impl MessageEditor {
waiting_for_summaries_to_send: false,
profile_selector: cx
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
last_estimated_token_count: None,
update_token_count_task: None,
_subscriptions: subscriptions,
}
}
@@ -219,7 +234,7 @@ impl MessageEditor {
}
self.set_editor_is_expanded(false, cx);
self.send_to_model(RequestKind::Chat, window, cx);
self.send_to_model(window, cx);
cx.notify();
}
@@ -234,12 +249,7 @@ impl MessageEditor {
.is_some()
}
fn send_to_model(
&mut self,
request_kind: RequestKind,
window: &mut Window,
cx: &mut Context<Self>,
) {
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {
return;
@@ -256,6 +266,9 @@ impl MessageEditor {
text
});
self.last_estimated_token_count.take();
cx.emit(MessageEditorEvent::EstimatedTokenCount);
let refresh_task =
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
@@ -275,6 +288,21 @@ impl MessageEditor {
})
.log_err();
context_store
.update(cx, |context_store, cx| {
let excerpt_ids = context_store
.context()
.iter()
.filter(|ctx| matches!(ctx, AssistantContext::Excerpt(_)))
.map(|ctx| ctx.id())
.collect::<Vec<_>>();
for id in excerpt_ids {
context_store.remove_context(id, cx);
}
})
.log_err();
if let Some(wait_for_summaries) = context_store
.update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
.log_err()
@@ -297,7 +325,8 @@ impl MessageEditor {
// Send to model after summaries are done
thread
.update(cx, |thread, cx| {
thread.send_to_model(model, request_kind, cx);
thread.advance_prompt_id();
thread.send_to_model(model, cx);
})
.log_err();
})
@@ -311,7 +340,7 @@ impl MessageEditor {
if cancelled {
self.set_editor_is_expanded(false, cx);
self.send_to_model(RequestKind::Chat, window, cx);
self.send_to_model(window, cx);
}
}
@@ -937,6 +966,82 @@ impl MessageEditor {
.label_size(LabelSize::Small),
)
}
pub fn last_estimated_token_count(&self) -> Option<usize> {
self.last_estimated_token_count
}
pub fn is_waiting_to_update_token_count(&self) -> bool {
self.update_token_count_task.is_some()
}
fn message_or_context_changed(&mut self, debounce: bool, cx: &mut Context<Self>) {
cx.emit(MessageEditorEvent::Changed);
self.update_token_count_task.take();
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
self.last_estimated_token_count.take();
return;
};
let context_store = self.context_store.clone();
let editor = self.editor.clone();
let thread = self.thread.clone();
self.update_token_count_task = Some(cx.spawn(async move |this, cx| {
if debounce {
cx.background_executor()
.timer(Duration::from_millis(200))
.await;
}
let token_count = if let Some(task) = cx.update(|cx| {
let context = context_store.read(cx).context().iter();
let new_context = thread.read(cx).filter_new_context(context);
let context_text =
format_context_as_string(new_context, cx).unwrap_or(String::new());
let message_text = editor.read(cx).text(cx);
let content = context_text + &message_text;
if content.is_empty() {
return None;
}
let request = language_model::LanguageModelRequest {
thread_id: None,
prompt_id: None,
messages: vec![LanguageModelRequestMessage {
role: language_model::Role::User,
content: vec![content.into()],
cache: false,
}],
tools: vec![],
stop: vec![],
temperature: None,
};
Some(default_model.model.count_tokens(request, cx))
})? {
task.await?
} else {
0
};
this.update(cx, |this, cx| {
this.last_estimated_token_count = Some(token_count);
cx.emit(MessageEditorEvent::EstimatedTokenCount);
this.update_token_count_task.take();
})
}));
}
}
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
pub enum MessageEditorEvent {
EstimatedTokenCount,
Changed,
}
impl Focusable for MessageEditor {
@@ -949,6 +1054,7 @@ impl Render for MessageEditor {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let thread = self.thread.read(cx);
let total_token_usage = thread.total_token_usage(cx);
let token_usage_ratio = total_token_usage.ratio();
let action_log = self.thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
@@ -997,15 +1103,8 @@ impl Render for MessageEditor {
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
})
.child(self.render_editor(font_size, line_height, window, cx))
.when(
total_token_usage.ratio != TokenUsageRatio::Normal,
|parent| {
parent.child(self.render_token_limit_callout(
line_height,
total_token_usage.ratio,
cx,
))
},
)
.when(token_usage_ratio != TokenUsageRatio::Normal, |parent| {
parent.child(self.render_token_limit_callout(line_height, token_usage_ratio, cx))
})
}
}

View File

@@ -261,6 +261,8 @@ impl TerminalInlineAssistant {
request_message.content.push(prompt.into());
Ok(LanguageModelRequest {
thread_id: None,
prompt_id: None,
messages: vec![request_message],
tools: Vec::new(),
stop: Vec::new(),

View File

@@ -4,9 +4,9 @@ use std::ops::Range;
use std::sync::Arc;
use std::time::Instant;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_settings::AssistantSettings;
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap};
use feature_flags::{self, FeatureFlagAppExt};
@@ -18,12 +18,14 @@ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason, TokenUsage,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, StopReason,
TokenUsage,
};
use project::Project;
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
use prompt_store::PromptBuilder;
use proto::Plan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -38,13 +40,6 @@ use crate::thread_store::{
};
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState, USING_TOOL_MARKER};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
Chat,
/// Used when summarizing a thread.
Summarize,
}
#[derive(
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,
)]
@@ -68,6 +63,24 @@ impl From<&str> for ThreadId {
}
}
/// The ID of the user prompt that initiated a request.
///
/// This equates to the user physically submitting a message to the model (e.g., by pressing the Enter key).
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
pub struct PromptId(Arc<str>);
impl PromptId {
pub fn new() -> Self {
Self(Uuid::new_v4().to_string().into())
}
}
impl std::fmt::Display for PromptId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct MessageId(pub(crate) usize);
@@ -93,12 +106,21 @@ impl Message {
self.segments.iter().all(|segment| segment.should_display())
}
pub fn push_thinking(&mut self, text: &str) {
if let Some(MessageSegment::Thinking(segment)) = self.segments.last_mut() {
pub fn push_thinking(&mut self, text: &str, signature: Option<String>) {
if let Some(MessageSegment::Thinking {
text: segment,
signature: current_signature,
}) = self.segments.last_mut()
{
if let Some(signature) = signature {
*current_signature = Some(signature);
}
segment.push_str(text);
} else {
self.segments
.push(MessageSegment::Thinking(text.to_string()));
self.segments.push(MessageSegment::Thinking {
text: text.to_string(),
signature,
});
}
}
@@ -120,11 +142,12 @@ impl Message {
for segment in &self.segments {
match segment {
MessageSegment::Text(text) => result.push_str(text),
MessageSegment::Thinking(text) => {
result.push_str("<think>");
MessageSegment::Thinking { text, .. } => {
result.push_str("<think>\n");
result.push_str(text);
result.push_str("</think>");
result.push_str("\n</think>");
}
MessageSegment::RedactedThinking(_) => {}
}
}
@@ -135,24 +158,22 @@ impl Message {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MessageSegment {
Text(String),
Thinking(String),
Thinking {
text: String,
signature: Option<String>,
},
RedactedThinking(Vec<u8>),
}
impl MessageSegment {
pub fn text_mut(&mut self) -> &mut String {
match self {
Self::Text(text) => text,
Self::Thinking(text) => text,
}
}
pub fn should_display(&self) -> bool {
// We add USING_TOOL_MARKER when making a request that includes tool uses
// without non-whitespace text around them, and this can cause the model
// to mimic the pattern, so we consider those segments not displayable.
match self {
Self::Text(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER,
Self::Thinking(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER,
Self::Thinking { text, .. } => text.is_empty() || text.trim() == USING_TOOL_MARKER,
Self::RedactedThinking(_) => false,
}
}
}
@@ -226,7 +247,33 @@ pub enum DetailedSummaryState {
pub struct TotalTokenUsage {
pub total: usize,
pub max: usize,
pub ratio: TokenUsageRatio,
}
impl TotalTokenUsage {
pub fn ratio(&self) -> TokenUsageRatio {
#[cfg(debug_assertions)]
let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
.unwrap_or("0.8".to_string())
.parse()
.unwrap();
#[cfg(not(debug_assertions))]
let warning_threshold: f32 = 0.8;
if self.total >= self.max {
TokenUsageRatio::Exceeded
} else if self.total as f32 / self.max as f32 >= warning_threshold {
TokenUsageRatio::Warning
} else {
TokenUsageRatio::Normal
}
}
pub fn add(&self, tokens: usize) -> TotalTokenUsage {
TotalTokenUsage {
total: self.total + tokens,
max: self.max,
}
}
}
#[derive(Debug, Default, PartialEq, Eq)]
@@ -246,6 +293,7 @@ pub struct Thread {
detailed_summary_state: DetailedSummaryState,
messages: Vec<Message>,
next_message_id: MessageId,
last_prompt_id: PromptId,
context: BTreeMap<ContextId, AssistantContext>,
context_by_message: HashMap<MessageId, Vec<ContextId>>,
project_context: SharedProjectContext,
@@ -260,11 +308,15 @@ pub struct Thread {
last_restore_checkpoint: Option<LastRestoreCheckpoint>,
pending_checkpoint: Option<ThreadCheckpoint>,
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
request_token_usage: Vec<TokenUsage>,
cumulative_token_usage: TokenUsage,
exceeded_window_error: Option<ExceededWindowError>,
feedback: Option<ThreadFeedback>,
message_feedback: HashMap<MessageId, ThreadFeedback>,
last_auto_capture_at: Option<Instant>,
request_callback: Option<
Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>,
>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -291,6 +343,7 @@ impl Thread {
detailed_summary_state: DetailedSummaryState::NotGenerated,
messages: Vec::new(),
next_message_id: MessageId(0),
last_prompt_id: PromptId::new(),
context: BTreeMap::default(),
context_by_message: HashMap::default(),
project_context: system_prompt,
@@ -310,11 +363,13 @@ impl Thread {
.spawn(async move { Some(project_snapshot.await) })
.shared()
},
request_token_usage: Vec::new(),
cumulative_token_usage: TokenUsage::default(),
exceeded_window_error: None,
feedback: None,
message_feedback: HashMap::default(),
last_auto_capture_at: None,
request_callback: None,
}
}
@@ -354,8 +409,11 @@ impl Thread {
.into_iter()
.map(|segment| match segment {
SerializedMessageSegment::Text { text } => MessageSegment::Text(text),
SerializedMessageSegment::Thinking { text } => {
MessageSegment::Thinking(text)
SerializedMessageSegment::Thinking { text, signature } => {
MessageSegment::Thinking { text, signature }
}
SerializedMessageSegment::RedactedThinking { data } => {
MessageSegment::RedactedThinking(data)
}
})
.collect(),
@@ -363,6 +421,7 @@ impl Thread {
})
.collect(),
next_message_id,
last_prompt_id: PromptId::new(),
context: BTreeMap::default(),
context_by_message: HashMap::default(),
project_context,
@@ -377,14 +436,24 @@ impl Thread {
tool_use,
action_log: cx.new(|_| ActionLog::new(project)),
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
request_token_usage: serialized.request_token_usage,
cumulative_token_usage: serialized.cumulative_token_usage,
exceeded_window_error: None,
feedback: None,
message_feedback: HashMap::default(),
last_auto_capture_at: None,
request_callback: None,
}
}
pub fn set_request_callback(
&mut self,
callback: impl 'static
+ FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>]),
) {
self.request_callback = Some(Box::new(callback));
}
pub fn id(&self) -> &ThreadId {
&self.id
}
@@ -401,6 +470,10 @@ impl Thread {
self.updated_at = Utc::now();
}
pub fn advance_prompt_id(&mut self) {
self.last_prompt_id = PromptId::new();
}
pub fn summary(&self) -> Option<SharedString> {
self.summary.clone()
}
@@ -630,10 +703,30 @@ impl Thread {
self.tool_use.tool_result(id)
}
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
Some(&self.tool_use.tool_result(id)?.content)
}
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<AnyToolCard> {
self.tool_use.tool_result_card(id).cloned()
}
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
self.tool_use.message_has_tool_results(message_id)
}
/// Filter out contexts that have already been included in previous messages
pub fn filter_new_context<'a>(
&self,
context: impl Iterator<Item = &'a AssistantContext>,
) -> impl Iterator<Item = &'a AssistantContext> {
context.filter(|ctx| self.is_context_new(ctx))
}
fn is_context_new(&self, context: &AssistantContext) -> bool {
!self.context.contains_key(&context.id())
}
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
@@ -645,10 +738,9 @@ impl Thread {
let message_id = self.insert_message(Role::User, vec![MessageSegment::Text(text)], cx);
// Filter out contexts that have already been included in previous messages
let new_context: Vec<_> = context
.into_iter()
.filter(|ctx| !self.context.contains_key(&ctx.id()))
.filter(|ctx| self.is_context_new(ctx))
.collect();
if !new_context.is_empty() {
@@ -676,6 +768,12 @@ impl Thread {
cx,
);
}
AssistantContext::Excerpt(excerpt_context) => {
log.buffer_added_as_context(
excerpt_context.context_buffer.buffer.clone(),
cx,
);
}
AssistantContext::FetchedUrl(_) | AssistantContext::Thread(_) => {}
}
}
@@ -768,9 +866,10 @@ impl Thread {
for segment in &message.segments {
match segment {
MessageSegment::Text(content) => text.push_str(content),
MessageSegment::Thinking(content) => {
MessageSegment::Thinking { text: content, .. } => {
text.push_str(&format!("<think>{}</think>", content))
}
MessageSegment::RedactedThinking(_) => {}
}
}
text.push('\n');
@@ -800,8 +899,16 @@ impl Thread {
MessageSegment::Text(text) => {
SerializedMessageSegment::Text { text: text.clone() }
}
MessageSegment::Thinking(text) => {
SerializedMessageSegment::Thinking { text: text.clone() }
MessageSegment::Thinking { text, signature } => {
SerializedMessageSegment::Thinking {
text: text.clone(),
signature: signature.clone(),
}
}
MessageSegment::RedactedThinking(data) => {
SerializedMessageSegment::RedactedThinking {
data: data.clone(),
}
}
})
.collect(),
@@ -828,19 +935,15 @@ impl Thread {
.collect(),
initial_project_snapshot,
cumulative_token_usage: this.cumulative_token_usage,
request_token_usage: this.request_token_usage.clone(),
detailed_summary_state: this.detailed_summary_state.clone(),
exceeded_window_error: this.exceeded_window_error.clone(),
})
})
}
pub fn send_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
request_kind: RequestKind,
cx: &mut Context<Self>,
) {
let mut request = self.to_completion_request(request_kind, cx);
pub fn send_to_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
let mut request = self.to_completion_request(cx);
if model.supports_tools() {
request.tools = {
let mut tools = Vec::new();
@@ -879,12 +982,10 @@ impl Thread {
false
}
pub fn to_completion_request(
&self,
request_kind: RequestKind,
cx: &App,
) -> LanguageModelRequest {
pub fn to_completion_request(&self, cx: &mut Context<Self>) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
thread_id: Some(self.id.to_string()),
prompt_id: Some(self.last_prompt_id.to_string()),
messages: vec![],
tools: Vec::new(),
stop: Vec::new(),
@@ -892,20 +993,33 @@ impl Thread {
};
if let Some(project_context) = self.project_context.borrow().as_ref() {
if let Some(system_prompt) = self
match self
.prompt_builder
.generate_assistant_system_prompt(project_context)
.context("failed to generate assistant system prompt")
.log_err()
{
request.messages.push(LanguageModelRequestMessage {
role: Role::System,
content: vec![MessageContent::Text(system_prompt)],
cache: true,
});
Err(err) => {
let message = format!("{err:?}").into();
log::error!("{message}");
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
header: "Error generating system prompt".into(),
message,
}));
}
Ok(system_prompt) => {
request.messages.push(LanguageModelRequestMessage {
role: Role::System,
content: vec![MessageContent::Text(system_prompt)],
cache: true,
});
}
}
} else {
log::error!("project_context not set.")
let message = "Context for system prompt unexpectedly not ready.".into();
log::error!("{message}");
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
header: "Error generating system prompt".into(),
message,
}));
}
for message in &self.messages {
@@ -915,34 +1029,42 @@ impl Thread {
cache: false,
};
match request_kind {
RequestKind::Chat => {
self.tool_use
.attach_tool_results(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
if self.tool_use.message_has_tool_results(message.id) {
continue;
}
}
}
self.tool_use
.attach_tool_results(message.id, &mut request_message);
if !message.segments.is_empty() {
if !message.context.is_empty() {
request_message
.content
.push(MessageContent::Text(message.to_string()));
.push(MessageContent::Text(message.context.to_string()));
}
match request_kind {
RequestKind::Chat => {
self.tool_use
.attach_tool_uses(message.id, &mut request_message);
}
RequestKind::Summarize => {
// We don't care about tool use during summarization.
}
};
for segment in &message.segments {
match segment {
MessageSegment::Text(text) => {
if !text.is_empty() {
request_message
.content
.push(MessageContent::Text(text.into()));
}
}
MessageSegment::Thinking { text, signature } => {
if !text.is_empty() {
request_message.content.push(MessageContent::Thinking {
text: text.into(),
signature: signature.clone(),
});
}
}
MessageSegment::RedactedThinking(data) => {
request_message
.content
.push(MessageContent::RedactedThinking(data.clone()));
}
};
}
self.tool_use
.attach_tool_uses(message.id, &mut request_message);
request.messages.push(request_message);
}
@@ -957,6 +1079,54 @@ impl Thread {
request
}
fn to_summarize_request(&self, added_user_message: String) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
messages: vec![],
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
};
for message in &self.messages {
let mut request_message = LanguageModelRequestMessage {
role: message.role,
content: Vec::new(),
cache: false,
};
// Skip tool results during summarization.
if self.tool_use.message_has_tool_results(message.id) {
continue;
}
for segment in &message.segments {
match segment {
MessageSegment::Text(text) => request_message
.content
.push(MessageContent::Text(text.clone())),
MessageSegment::Thinking { .. } => {}
MessageSegment::RedactedThinking(_) => {}
}
}
if request_message.content.is_empty() {
continue;
}
request.messages.push(request_message);
}
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text(added_user_message)],
cache: false,
});
request
}
fn attached_tracked_files_state(
&self,
messages: &mut Vec<LanguageModelRequestMessage>,
@@ -986,15 +1156,6 @@ impl Thread {
content.push(stale_message.into());
}
if action_log.has_edited_files_since_project_diagnostics_check() {
content.push(
"\n\nWhen you're done making changes, make sure to check project diagnostics \
and fix all errors AND warnings you introduced! \
DO NOT mention you're going to do this until you're done."
.into(),
);
}
if !content.is_empty() {
let context_message = LanguageModelRequestMessage {
role: Role::User,
@@ -1013,17 +1174,36 @@ impl Thread {
cx: &mut Context<Self>,
) {
let pending_completion_id = post_inc(&mut self.completion_count);
let mut request_callback_parameters = if self.request_callback.is_some() {
Some((request.clone(), Vec::new()))
} else {
None
};
let prompt_id = self.last_prompt_id.clone();
let task = cx.spawn(async move |thread, cx| {
let stream = model.stream_completion(request, &cx);
let stream_completion_future = model.stream_completion_with_usage(request, &cx);
let initial_token_usage =
thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage);
let stream_completion = async {
let mut events = stream.await?;
let (mut events, usage) = stream_completion_future.await?;
let mut stop_reason = StopReason::EndTurn;
let mut current_token_usage = TokenUsage::default();
if let Some(usage) = usage {
thread
.update(cx, |_thread, cx| {
cx.emit(ThreadEvent::UsageUpdated(usage));
})
.ok();
}
while let Some(event) = events.next().await {
if let Some((_, response_events)) = request_callback_parameters.as_mut() {
response_events
.push(event.as_ref().map_err(|error| error.to_string()).cloned());
}
let event = event?;
thread.update(cx, |thread, cx| {
@@ -1039,6 +1219,7 @@ impl Thread {
stop_reason = reason;
}
LanguageModelCompletionEvent::UsageUpdate(token_usage) => {
thread.update_token_usage_at_last_message(token_usage);
thread.cumulative_token_usage = thread.cumulative_token_usage
+ token_usage
- current_token_usage;
@@ -1066,10 +1247,13 @@ impl Thread {
};
}
}
LanguageModelCompletionEvent::Thinking(chunk) => {
LanguageModelCompletionEvent::Thinking {
text: chunk,
signature,
} => {
if let Some(last_message) = thread.messages.last_mut() {
if last_message.role == Role::Assistant {
last_message.push_thinking(&chunk);
last_message.push_thinking(&chunk, signature);
cx.emit(ThreadEvent::StreamedAssistantThinking(
last_message.id,
chunk,
@@ -1082,7 +1266,10 @@ impl Thread {
// will result in duplicating the text of the chunk in the rendered Markdown.
thread.insert_message(
Role::Assistant,
vec![MessageSegment::Thinking(chunk.to_string())],
vec![MessageSegment::Thinking {
text: chunk.to_string(),
signature,
}],
cx,
);
};
@@ -1121,7 +1308,12 @@ impl Thread {
.pending_completions
.retain(|completion| completion.id != pending_completion_id);
if thread.summary.is_none() && thread.messages.len() >= 2 {
// If there is a response without tool use, summarize the message. Otherwise,
// allow two tool uses before summarizing.
if thread.summary.is_none()
&& thread.messages.len() >= 2
&& (!thread.has_pending_tool_uses() || thread.messages.len() >= 6)
{
thread.summarize(cx);
}
})?;
@@ -1150,6 +1342,12 @@ impl Thread {
cx.emit(ThreadEvent::ShowError(
ThreadError::MaxMonthlySpendReached,
));
} else if let Some(error) =
error.downcast_ref::<ModelRequestLimitReachedError>()
{
cx.emit(ThreadEvent::ShowError(
ThreadError::ModelRequestLimitReached { plan: error.plan },
));
} else if let Some(known_error) =
error.downcast_ref::<LanguageModelKnownError>()
{
@@ -1181,6 +1379,14 @@ impl Thread {
}
cx.emit(ThreadEvent::Stopped(result.map_err(Arc::new)));
if let Some((request_callback, (request, response_events))) = thread
.request_callback
.as_mut()
.zip(request_callback_parameters.as_ref())
{
request_callback(request, response_events);
}
thread.auto_capture_telemetry(cx);
if let Ok(initial_usage) = initial_token_usage {
@@ -1189,6 +1395,7 @@ impl Thread {
telemetry::event!(
"Assistant Thread Completion",
thread_id = thread.id().to_string(),
prompt_id = prompt_id,
model = model.telemetry_id(),
model_provider = model.provider_id().to_string(),
input_tokens = usage.input_tokens,
@@ -1216,23 +1423,24 @@ impl Thread {
return;
}
let mut request = self.to_completion_request(RequestKind::Summarize, cx);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
"Generate a concise 3-7 word title for this conversation, omitting punctuation. \
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. \
If the conversation is about a specific subject, include it in the title. \
Be descriptive. DO NOT speak in the first person."
.into(),
],
cache: false,
});
let added_user_message = "Generate a concise 3-7 word title for this conversation, omitting punctuation. \
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. \
If the conversation is about a specific subject, include it in the title. \
Be descriptive. DO NOT speak in the first person.";
let request = self.to_summarize_request(added_user_message.into());
self.pending_summary = cx.spawn(async move |this, cx| {
async move {
let stream = model.model.stream_completion_text(request, &cx);
let mut messages = stream.await?;
let stream = model.model.stream_completion_text_with_usage(request, &cx);
let (mut messages, usage) = stream.await?;
if let Some(usage) = usage {
this.update(cx, |_thread, cx| {
cx.emit(ThreadEvent::UsageUpdated(usage));
})
.ok();
}
let mut new_summary = String::new();
while let Some(message) = messages.stream.next().await {
@@ -1282,21 +1490,14 @@ impl Thread {
return None;
}
let mut request = self.to_completion_request(RequestKind::Summarize, cx);
let added_user_message = "Generate a detailed summary of this conversation. Include:\n\
1. A brief overview of what was discussed\n\
2. Key facts or information discovered\n\
3. Outcomes or conclusions reached\n\
4. Any action items or next steps if any\n\
Format it in Markdown with headings and bullet points.";
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
"Generate a detailed summary of this conversation. Include:\n\
1. A brief overview of what was discussed\n\
2. Key facts or information discovered\n\
3. Outcomes or conclusions reached\n\
4. Any action items or next steps if any\n\
Format it in Markdown with headings and bullet points."
.into(),
],
cache: false,
});
let request = self.to_summarize_request(added_user_message.into());
let task = cx.spawn(async move |thread, cx| {
let stream = model.stream_completion_text(request, &cx);
@@ -1344,7 +1545,7 @@ impl Thread {
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) -> Vec<PendingToolUse> {
self.auto_capture_telemetry(cx);
let request = self.to_completion_request(RequestKind::Chat, cx);
let request = self.to_completion_request(cx);
let messages = Arc::new(request.messages);
let pending_tool_uses = self
.tool_use
@@ -1419,6 +1620,12 @@ impl Thread {
)
};
// Store the card separately if it exists
if let Some(card) = tool_result.card.clone() {
self.tool_use
.insert_tool_result_card(tool_use_id.clone(), card);
}
cx.spawn({
async move |thread: WeakEntity<Thread>, cx| {
let output = tool_result.output.await;
@@ -1450,7 +1657,7 @@ impl Thread {
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
self.attach_tool_results(cx);
if !canceled {
self.send_to_model(model, RequestKind::Chat, cx);
self.send_to_model(model, cx);
}
}
}
@@ -1461,17 +1668,12 @@ impl Thread {
});
}
/// Insert an empty message to be populated with tool results upon send.
pub fn attach_tool_results(&mut self, cx: &mut Context<Self>) {
// Insert a user message to contain the tool results.
self.insert_user_message(
// TODO: Sending up a user message without any content results in the model sending back
// responses that also don't have any content. We currently don't handle this case well,
// so for now we provide some text to keep the model on track.
"Here are the tool results.",
Vec::new(),
None,
cx,
);
// Tool results are assumed to be waiting on the next message id, so they will populate
// this empty message before sending to model. Would prefer this to be more straightforward.
self.insert_message(Role::User, vec![], cx);
self.auto_capture_telemetry(cx);
}
/// Cancels the last pending completion, if there are any pending.
@@ -1750,9 +1952,10 @@ impl Thread {
for segment in &message.segments {
match segment {
MessageSegment::Text(text) => writeln!(markdown, "{}\n", text)?,
MessageSegment::Thinking(text) => {
writeln!(markdown, "<think>{}</think>\n", text)?
MessageSegment::Thinking { text, .. } => {
writeln!(markdown, "<think>\n{}\n</think>\n", text)?
}
MessageSegment::RedactedThinking(_) => {}
}
}
@@ -1801,14 +2004,14 @@ impl Thread {
.update(cx, |action_log, cx| action_log.keep_all_edits(cx));
}
pub fn reject_edits_in_range(
pub fn reject_edits_in_ranges(
&mut self,
buffer: Entity<language::Buffer>,
buffer_range: Range<language::Anchor>,
buffer_ranges: Vec<Range<language::Anchor>>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.action_log.update(cx, |action_log, cx| {
action_log.reject_edits_in_range(buffer, buffer_range, cx)
action_log.reject_edits_in_ranges(buffer, buffer_ranges, cx)
})
}
@@ -1868,6 +2071,35 @@ impl Thread {
self.cumulative_token_usage
}
pub fn token_usage_up_to_message(&self, message_id: MessageId, cx: &App) -> TotalTokenUsage {
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
return TotalTokenUsage::default();
};
let max = model.model.max_token_count();
let index = self
.messages
.iter()
.position(|msg| msg.id == message_id)
.unwrap_or(0);
if index == 0 {
return TotalTokenUsage { total: 0, max };
}
let token_usage = &self
.request_token_usage
.get(index - 1)
.cloned()
.unwrap_or_default();
TotalTokenUsage {
total: token_usage.total_tokens() as usize,
max,
}
}
pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(model) = model_registry.default_model() else {
@@ -1881,30 +2113,33 @@ impl Thread {
return TotalTokenUsage {
total: exceeded_error.token_count,
max,
ratio: TokenUsageRatio::Exceeded,
};
}
}
#[cfg(debug_assertions)]
let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
.unwrap_or("0.8".to_string())
.parse()
.unwrap();
#[cfg(not(debug_assertions))]
let warning_threshold: f32 = 0.8;
let total = self
.token_usage_at_last_message()
.unwrap_or_default()
.total_tokens() as usize;
let total = self.cumulative_token_usage.total_tokens() as usize;
TotalTokenUsage { total, max }
}
let ratio = if total >= max {
TokenUsageRatio::Exceeded
} else if total as f32 / max as f32 >= warning_threshold {
TokenUsageRatio::Warning
} else {
TokenUsageRatio::Normal
};
fn token_usage_at_last_message(&self) -> Option<TokenUsage> {
self.request_token_usage
.get(self.messages.len().saturating_sub(1))
.or_else(|| self.request_token_usage.last())
.cloned()
}
TotalTokenUsage { total, max, ratio }
fn update_token_usage_at_last_message(&mut self, token_usage: TokenUsage) {
let placeholder = self.token_usage_at_last_message().unwrap_or_default();
self.request_token_usage
.resize(self.messages.len(), placeholder);
if let Some(last) = self.request_token_usage.last_mut() {
*last = token_usage;
}
}
pub fn deny_tool_use(
@@ -1929,6 +2164,8 @@ pub enum ThreadError {
PaymentRequired,
#[error("Max monthly spend reached")]
MaxMonthlySpendReached,
#[error("Model request limit reached")]
ModelRequestLimitReached { plan: Plan },
#[error("Message {header}: {message}")]
Message {
header: SharedString,
@@ -1939,6 +2176,7 @@ pub enum ThreadError {
#[derive(Debug, Clone)]
pub enum ThreadEvent {
ShowError(ThreadError),
UsageUpdated(RequestUsage),
StreamedCompletion,
StreamedAssistantText(MessageId, String),
StreamedAssistantThinking(MessageId, String),
@@ -2044,9 +2282,7 @@ fn main() {{
assert_eq!(message.context, expected_context);
// Check message in request
let request = thread.read_with(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
assert_eq!(request.messages.len(), 2);
let expected_full_message = format!("{}Please explain this code", expected_context);
@@ -2136,9 +2372,7 @@ fn main() {{
assert!(message3.context.contains("file3.rs"));
// Check entire request to make sure all contexts are properly included
let request = thread.read_with(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
// The request should contain all 3 messages
assert_eq!(request.messages.len(), 4);
@@ -2188,9 +2422,7 @@ fn main() {{
assert_eq!(message.context, "");
// Check message in request
let request = thread.read_with(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
assert_eq!(request.messages.len(), 2);
assert_eq!(
@@ -2208,9 +2440,7 @@ fn main() {{
assert_eq!(message2.context, "");
// Check that both messages appear in the request
let request = thread.read_with(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
assert_eq!(request.messages.len(), 3);
assert_eq!(
@@ -2250,9 +2480,7 @@ fn main() {{
});
// Create a request and check that it doesn't have a stale buffer warning yet
let initial_request = thread.read_with(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
let initial_request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
// Make sure we don't have a stale file warning yet
let has_stale_warning = initial_request.messages.iter().any(|msg| {
@@ -2280,9 +2508,7 @@ fn main() {{
});
// Create a new request and check for the stale buffer warning
let new_request = thread.read_with(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
let new_request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
// We should have a stale file warning as the last message
let last_message = new_request
@@ -2309,6 +2535,7 @@ fn main() {{
language::init(cx);
Project::init_settings(cx);
AssistantSettings::register(cx);
prompt_store::init(cx);
thread_store::init(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
@@ -2348,7 +2575,8 @@ fn main() {{
cx,
)
})
.await;
.await
.unwrap();
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));

View File

@@ -12,8 +12,9 @@ use collections::HashMap;
use context_server::manager::ContextServerManager;
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use fs::Fs;
use futures::FutureExt as _;
use futures::channel::{mpsc, oneshot};
use futures::future::{self, BoxFuture, Shared};
use futures::{FutureExt as _, StreamExt as _};
use gpui::{
App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString,
Subscription, Task, prelude::*,
@@ -22,7 +23,10 @@ use heed::Database;
use heed::types::SerdeBincode;
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
use project::{Project, Worktree};
use prompt_store::{ProjectContext, PromptBuilder, RulesFileContext, WorktreeContext};
use prompt_store::{
DefaultUserRulesContext, ProjectContext, PromptBuilder, PromptId, PromptStore,
PromptsUpdatedEvent, RulesFileContext, WorktreeContext,
};
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
use util::ResultExt as _;
@@ -62,6 +66,8 @@ pub struct ThreadStore {
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
threads: Vec<SerializedThreadMetadata>,
project_context: SharedProjectContext,
reload_system_prompt_tx: mpsc::Sender<()>,
_reload_system_prompt_task: Task<()>,
_subscriptions: Vec<Subscription>,
}
@@ -77,12 +83,22 @@ impl ThreadStore {
tools: Entity<ToolWorkingSet>,
prompt_builder: Arc<PromptBuilder>,
cx: &mut App,
) -> Task<Entity<Self>> {
let thread_store = cx.new(|cx| Self::new(project, tools, prompt_builder, cx));
let reload = thread_store.update(cx, |store, cx| store.reload_system_prompt(cx));
cx.foreground_executor().spawn(async move {
reload.await;
thread_store
) -> Task<Result<Entity<Self>>> {
let prompt_store = PromptStore::global(cx);
cx.spawn(async move |cx| {
let prompt_store = prompt_store.await.ok();
let (thread_store, ready_rx) = cx.update(|cx| {
let mut option_ready_rx = None;
let thread_store = cx.new(|cx| {
let (thread_store, ready_rx) =
Self::new(project, tools, prompt_builder, prompt_store, cx);
option_ready_rx = Some(ready_rx);
thread_store
});
(thread_store, option_ready_rx.take().unwrap())
})?;
ready_rx.await?;
Ok(thread_store)
})
}
@@ -90,17 +106,53 @@ impl ThreadStore {
project: Entity<Project>,
tools: Entity<ToolWorkingSet>,
prompt_builder: Arc<PromptBuilder>,
prompt_store: Option<Entity<PromptStore>>,
cx: &mut Context<Self>,
) -> Self {
) -> (Self, oneshot::Receiver<()>) {
let context_server_factory_registry = ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
});
let settings_subscription =
let mut subscriptions = vec![
cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
this.load_default_profile(cx);
});
let project_subscription = cx.subscribe(&project, Self::handle_project_event);
}),
cx.subscribe(&project, Self::handle_project_event),
];
if let Some(prompt_store) = prompt_store.as_ref() {
subscriptions.push(cx.subscribe(
prompt_store,
|this, _prompt_store, PromptsUpdatedEvent, _cx| {
this.enqueue_system_prompt_reload();
},
))
}
// This channel and task prevent concurrent and redundant loading of the system prompt.
let (reload_system_prompt_tx, mut reload_system_prompt_rx) = mpsc::channel(1);
let (ready_tx, ready_rx) = oneshot::channel();
let mut ready_tx = Some(ready_tx);
let reload_system_prompt_task = cx.spawn({
async move |thread_store, cx| {
loop {
let Some(reload_task) = thread_store
.update(cx, |thread_store, cx| {
thread_store.reload_system_prompt(prompt_store.clone(), cx)
})
.ok()
else {
return;
};
reload_task.await;
if let Some(ready_tx) = ready_tx.take() {
ready_tx.send(()).ok();
}
reload_system_prompt_rx.next().await;
}
}
});
let this = Self {
project,
@@ -110,23 +162,25 @@ impl ThreadStore {
context_server_tool_ids: HashMap::default(),
threads: Vec::new(),
project_context: SharedProjectContext::default(),
_subscriptions: vec![settings_subscription, project_subscription],
reload_system_prompt_tx,
_reload_system_prompt_task: reload_system_prompt_task,
_subscriptions: subscriptions,
};
this.load_default_profile(cx);
this.register_context_server_handlers(cx);
this.reload(cx).detach_and_log_err(cx);
this
(this, ready_rx)
}
fn handle_project_event(
&mut self,
_project: Entity<Project>,
event: &project::Event,
cx: &mut Context<Self>,
_cx: &mut Context<Self>,
) {
match event {
project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => {
self.reload_system_prompt(cx).detach();
self.enqueue_system_prompt_reload();
}
project::Event::WorktreeUpdatedEntries(_, items) => {
if items.iter().any(|(path, _, _)| {
@@ -134,16 +188,25 @@ impl ThreadStore {
.iter()
.any(|name| path.as_ref() == Path::new(name))
}) {
self.reload_system_prompt(cx).detach();
self.enqueue_system_prompt_reload();
}
}
_ => {}
}
}
pub fn reload_system_prompt(&self, cx: &mut Context<Self>) -> Task<()> {
fn enqueue_system_prompt_reload(&mut self) {
self.reload_system_prompt_tx.try_send(()).ok();
}
// Note that this should only be called from `reload_system_prompt_task`.
fn reload_system_prompt(
&self,
prompt_store: Option<Entity<PromptStore>>,
cx: &mut Context<Self>,
) -> Task<()> {
let project = self.project.read(cx);
let tasks = project
let worktree_tasks = project
.visible_worktrees(cx)
.map(|worktree| {
Self::load_worktree_info_for_system_prompt(
@@ -153,10 +216,23 @@ impl ThreadStore {
)
})
.collect::<Vec<_>>();
let default_user_rules_task = match prompt_store {
None => Task::ready(vec![]),
Some(prompt_store) => prompt_store.read_with(cx, |prompt_store, cx| {
let prompts = prompt_store.default_prompt_metadata();
let load_tasks = prompts.into_iter().map(|prompt_metadata| {
let contents = prompt_store.load(prompt_metadata.id, cx);
async move { (contents.await, prompt_metadata) }
});
cx.background_spawn(future::join_all(load_tasks))
}),
};
cx.spawn(async move |this, cx| {
let results = futures::future::join_all(tasks).await;
let worktrees = results
let (worktrees, default_user_rules) =
future::join(future::join_all(worktree_tasks), default_user_rules_task).await;
let worktrees = worktrees
.into_iter()
.map(|(worktree, rules_error)| {
if let Some(rules_error) = rules_error {
@@ -165,8 +241,33 @@ impl ThreadStore {
worktree
})
.collect::<Vec<_>>();
let default_user_rules = default_user_rules
.into_iter()
.flat_map(|(contents, prompt_metadata)| match contents {
Ok(contents) => Some(DefaultUserRulesContext {
uuid: match prompt_metadata.id {
PromptId::User { uuid } => uuid,
PromptId::EditWorkflow => return None,
},
title: prompt_metadata.title.map(|title| title.to_string()),
contents,
}),
Err(err) => {
this.update(cx, |_, cx| {
cx.emit(RulesLoadingError {
message: format!("{err:?}").into(),
});
})
.ok();
None
}
})
.collect::<Vec<_>>();
this.update(cx, |this, _cx| {
*this.project_context.0.borrow_mut() = Some(ProjectContext::new(worktrees));
*this.project_context.0.borrow_mut() =
Some(ProjectContext::new(worktrees, default_user_rules));
})
.ok();
})
@@ -178,14 +279,12 @@ impl ThreadStore {
cx: &App,
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
let root_name = worktree.root_name().into();
let abs_path = worktree.abs_path();
let rules_task = Self::load_worktree_rules_file(fs, worktree, cx);
let Some(rules_task) = rules_task else {
return Task::ready((
WorktreeContext {
root_name,
abs_path,
rules_file: None,
},
None,
@@ -204,7 +303,6 @@ impl ThreadStore {
};
let worktree_info = WorktreeContext {
root_name,
abs_path,
rules_file,
};
(worktree_info, rules_file_error)
@@ -509,6 +607,8 @@ pub struct SerializedThread {
#[serde(default)]
pub cumulative_token_usage: TokenUsage,
#[serde(default)]
pub request_token_usage: Vec<TokenUsage>,
#[serde(default)]
pub detailed_summary_state: DetailedSummaryState,
#[serde(default)]
pub exceeded_window_error: Option<ExceededWindowError>,
@@ -560,9 +660,18 @@ pub struct SerializedMessage {
#[serde(tag = "type")]
pub enum SerializedMessageSegment {
#[serde(rename = "text")]
Text { text: String },
Text {
text: String,
},
#[serde(rename = "thinking")]
Thinking { text: String },
Thinking {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
RedactedThinking {
data: Vec<u8>,
},
}
#[derive(Debug, Serialize, Deserialize)]
@@ -597,6 +706,7 @@ impl LegacySerializedThread {
messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(),
initial_project_snapshot: self.initial_project_snapshot,
cumulative_token_usage: TokenUsage::default(),
request_token_usage: Vec::new(),
detailed_summary_state: DetailedSummaryState::default(),
exceeded_window_error: None,
}

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use assistant_tool::{Tool, ToolWorkingSet, ToolWorkingSetEvent};
use assistant_tool::{Tool, ToolSource, ToolWorkingSet, ToolWorkingSetEvent};
use collections::HashMap;
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
@@ -73,7 +73,12 @@ impl Render for IncompatibleToolsTooltip {
.children(
self.incompatible_tools
.iter()
.map(|tool| Label::new(tool.name()).size(LabelSize::Small).buffer_font(cx)),
.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))

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::{Tool, ToolWorkingSet};
use assistant_tool::{AnyToolCard, Tool, ToolUseStatus, ToolWorkingSet};
use collections::HashMap;
use futures::FutureExt as _;
use futures::future::Shared;
@@ -27,26 +27,7 @@ pub struct ToolUse {
pub needs_confirmation: bool,
}
#[derive(Debug, Clone)]
pub enum ToolUseStatus {
NeedsConfirmation,
Pending,
Running,
Finished(SharedString),
Error(SharedString),
}
impl ToolUseStatus {
pub fn text(&self) -> SharedString {
match self {
ToolUseStatus::NeedsConfirmation => "".into(),
ToolUseStatus::Pending => "".into(),
ToolUseStatus::Running => "".into(),
ToolUseStatus::Finished(out) => out.clone(),
ToolUseStatus::Error(out) => out.clone(),
}
}
}
pub const USING_TOOL_MARKER: &str = "<using_tool>";
pub struct ToolUseState {
tools: Entity<ToolWorkingSet>,
@@ -54,10 +35,9 @@ pub struct ToolUseState {
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
}
pub const USING_TOOL_MARKER: &str = "<using_tool>";
impl ToolUseState {
pub fn new(tools: Entity<ToolWorkingSet>) -> Self {
Self {
@@ -66,6 +46,7 @@ impl ToolUseState {
tool_uses_by_user_message: HashMap::default(),
tool_results: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
tool_result_cards: HashMap::default(),
}
}
@@ -257,6 +238,18 @@ impl ToolUseState {
self.tool_results.get(tool_use_id)
}
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> {
self.tool_result_cards.get(tool_use_id)
}
pub fn insert_tool_result_card(
&mut self,
tool_use_id: LanguageModelToolUseId,
card: AnyToolCard,
) {
self.tool_result_cards.insert(tool_use_id, card);
}
pub fn request_tool_use(
&mut self,
assistant_message_id: MessageId,

View File

@@ -1,7 +1,7 @@
mod agent_notification;
mod context_pill;
mod user_spending;
mod usage_banner;
pub use agent_notification::*;
pub use context_pill::*;
// pub use user_spending::*;
pub use usage_banner::*;

View File

@@ -191,15 +191,12 @@ impl RenderOnce for ContextPill {
ContextPill::Suggested {
name,
icon_path: _,
kind,
kind: _,
focused,
on_click,
} => base_pill
.cursor_pointer()
.pr_1()
.when(*focused, |this| {
this.bg(color.element_background.opacity(0.5))
})
.border_dashed()
.border_color(if *focused {
color.border_focused
@@ -207,30 +204,17 @@ impl RenderOnce for ContextPill {
color.border
})
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
.when(*focused, |this| {
this.bg(color.element_background.opacity(0.5))
})
.child(
div().px_0p5().max_w_64().child(
div().max_w_64().child(
Label::new(name.clone())
.size(LabelSize::Small)
.color(Color::Muted)
.truncate(),
),
)
.child(
Label::new(match kind {
ContextKind::File => "Active Tab",
ContextKind::Thread
| ContextKind::Directory
| ContextKind::FetchedUrl
| ContextKind::Symbol => "Active",
})
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(
Icon::new(IconName::Plus)
.size(IconSize::XSmall)
.into_any_element(),
)
.tooltip(|window, cx| {
Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
})
@@ -315,6 +299,39 @@ impl AddedContext {
summarizing: false,
},
AssistantContext::Excerpt(excerpt_context) => {
let full_path = excerpt_context.context_buffer.file.full_path(cx);
let mut full_path_string = full_path.to_string_lossy().into_owned();
let mut name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| full_path_string.clone());
let line_range_text = format!(
" ({}-{})",
excerpt_context.line_range.start.row + 1,
excerpt_context.line_range.end.row + 1
);
full_path_string.push_str(&line_range_text);
name.push_str(&line_range_text);
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
id: excerpt_context.id,
kind: ContextKind::File, // Use File icon for excerpts
name: name.into(),
parent,
tooltip: Some(full_path_string.into()),
icon_path: FileIcons::get_icon(&full_path, cx),
summarizing: false,
}
}
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
id: fetched_url_context.id,
kind: ContextKind::FetchedUrl,

View File

@@ -0,0 +1,202 @@
use client::zed_urls;
use ui::{Banner, ProgressBar, Severity, prelude::*};
use zed_llm_client::{Plan, UsageLimit};
#[derive(IntoElement, RegisterComponent)]
pub struct UsageBanner {
plan: Plan,
requests: i32,
}
impl UsageBanner {
pub fn new(plan: Plan, requests: i32) -> Self {
Self { plan, requests }
}
}
impl RenderOnce for UsageBanner {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let request_limit = self.plan.model_requests_limit();
let used_percentage = match request_limit {
UsageLimit::Limited(limit) => Some((self.requests as f32 / limit as f32) * 100.),
UsageLimit::Unlimited => None,
};
let (severity, message) = match request_limit {
UsageLimit::Limited(limit) => {
if self.requests >= limit {
let message = match self.plan {
Plan::ZedPro => "Monthly request limit reached",
Plan::ZedProTrial => "Trial request limit reached",
Plan::Free => "Free tier request limit reached",
};
(Severity::Error, message)
} else if (self.requests as f32 / limit as f32) >= 0.9 {
(Severity::Warning, "Approaching request limit")
} else {
let message = match self.plan {
Plan::ZedPro => "Zed Pro",
Plan::ZedProTrial => "Zed Pro (Trial)",
Plan::Free => "Zed Free",
};
(Severity::Info, message)
}
}
UsageLimit::Unlimited => {
let message = match self.plan {
Plan::ZedPro => "Zed Pro",
Plan::ZedProTrial => "Zed Pro (Trial)",
Plan::Free => "Zed Free",
};
(Severity::Info, message)
}
};
let action = match self.plan {
Plan::ZedProTrial | Plan::Free => {
Button::new("upgrade", "Upgrade").on_click(|_, _window, cx| {
cx.open_url(&zed_urls::account_url(cx));
})
}
Plan::ZedPro => Button::new("manage", "Manage").on_click(|_, _window, cx| {
cx.open_url(&zed_urls::account_url(cx));
}),
};
Banner::new().severity(severity).children(
h_flex().flex_1().gap_1().child(Label::new(message)).child(
h_flex()
.flex_1()
.justify_end()
.gap_1p5()
.children(used_percentage.map(|percent| {
h_flex()
.items_center()
.w_full()
.max_w(px(180.))
.child(ProgressBar::new("usage", percent, 100., cx))
}))
.child(
Label::new(match request_limit {
UsageLimit::Limited(limit) => {
format!("{} / {limit}", self.requests)
}
UsageLimit::Unlimited => format!("{} / ∞", self.requests),
})
.size(LabelSize::Small)
.color(Color::Muted),
)
// Note: This should go in the banner's `action_slot`, but doing that messes with the size of the
// progress bar.
.child(action),
),
)
}
}
impl Component for UsageBanner {
fn sort_name() -> &'static str {
"AgentUsageBanner"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let trial_examples = vec![
single_example(
"Zed Pro Trial - New User",
div()
.size_full()
.child(UsageBanner::new(Plan::ZedProTrial, 10))
.into_any_element(),
),
single_example(
"Zed Pro Trial - Approaching Limit",
div()
.size_full()
.child(UsageBanner::new(Plan::ZedProTrial, 135))
.into_any_element(),
),
single_example(
"Zed Pro Trial - Request Limit Reached",
div()
.size_full()
.child(UsageBanner::new(Plan::ZedProTrial, 150))
.into_any_element(),
),
];
let free_examples = vec![
single_example(
"Free - Normal Usage",
div()
.size_full()
.child(UsageBanner::new(Plan::Free, 25))
.into_any_element(),
),
single_example(
"Free - Approaching Limit",
div()
.size_full()
.child(UsageBanner::new(Plan::Free, 45))
.into_any_element(),
),
single_example(
"Free - Request Limit Reached",
div()
.size_full()
.child(UsageBanner::new(Plan::Free, 50))
.into_any_element(),
),
];
let zed_pro_examples = vec![
single_example(
"Zed Pro - Normal Usage",
div()
.size_full()
.child(UsageBanner::new(Plan::ZedPro, 250))
.into_any_element(),
),
single_example(
"Zed Pro - Approaching Limit",
div()
.size_full()
.child(UsageBanner::new(Plan::ZedPro, 450))
.into_any_element(),
),
single_example(
"Zed Pro - Request Limit Reached",
div()
.size_full()
.child(UsageBanner::new(Plan::ZedPro, 500))
.into_any_element(),
),
];
Some(
v_flex()
.gap_6()
.p_4()
.children(vec![
Label::new("Trial Plan")
.size(LabelSize::Large)
.into_any_element(),
example_group(trial_examples).vertical().into_any_element(),
Label::new("Free Plan")
.size(LabelSize::Large)
.into_any_element(),
example_group(free_examples).vertical().into_any_element(),
Label::new("Pro Plan")
.size(LabelSize::Large)
.into_any_element(),
example_group(zed_pro_examples)
.vertical()
.into_any_element(),
])
.into_any_element(),
)
}
}

View File

@@ -1,186 +0,0 @@
use gpui::{Entity, Render};
use ui::{ProgressBar, prelude::*};
#[derive(RegisterComponent)]
pub struct UserSpending {
free_tier_current: u32,
free_tier_cap: u32,
over_tier_current: u32,
over_tier_cap: u32,
free_tier_progress: Entity<ProgressBar>,
over_tier_progress: Entity<ProgressBar>,
}
impl UserSpending {
pub fn new(
free_tier_current: u32,
free_tier_cap: u32,
over_tier_current: u32,
over_tier_cap: u32,
cx: &mut App,
) -> Self {
let free_tier_capped = free_tier_current == free_tier_cap;
let free_tier_near_capped =
free_tier_current as f32 / 100.0 >= free_tier_cap as f32 / 100.0 * 0.9;
let over_tier_capped = over_tier_current == over_tier_cap;
let over_tier_near_capped =
over_tier_current as f32 / 100.0 >= over_tier_cap as f32 / 100.0 * 0.9;
let free_tier_progress = cx.new(|cx| {
ProgressBar::new(
"free_tier",
free_tier_current as f32,
free_tier_cap as f32,
cx,
)
});
let over_tier_progress = cx.new(|cx| {
ProgressBar::new(
"over_tier",
over_tier_current as f32,
over_tier_cap as f32,
cx,
)
});
if free_tier_capped {
free_tier_progress.update(cx, |progress_bar, cx| {
progress_bar.fg_color(cx.theme().status().error);
});
} else if free_tier_near_capped {
free_tier_progress.update(cx, |progress_bar, cx| {
progress_bar.fg_color(cx.theme().status().warning);
});
}
if over_tier_capped {
over_tier_progress.update(cx, |progress_bar, cx| {
progress_bar.fg_color(cx.theme().status().error);
});
} else if over_tier_near_capped {
over_tier_progress.update(cx, |progress_bar, cx| {
progress_bar.fg_color(cx.theme().status().warning);
});
}
Self {
free_tier_current,
free_tier_cap,
over_tier_current,
over_tier_cap,
free_tier_progress,
over_tier_progress,
}
}
}
impl Render for UserSpending {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let formatted_free_tier = format!(
"${} / ${}",
self.free_tier_current as f32 / 100.0,
self.free_tier_cap as f32 / 100.0
);
let formatted_over_tier = format!(
"${} / ${}",
self.over_tier_current as f32 / 100.0,
self.over_tier_cap as f32 / 100.0
);
v_group()
.elevation_2(cx)
.py_1p5()
.px_2p5()
.w(px(360.))
.child(
v_flex()
.child(
v_flex()
.p_1p5()
.gap_0p5()
.child(
h_flex()
.justify_between()
.child(Label::new("Free Tier Usage").size(LabelSize::Small))
.child(
Label::new(formatted_free_tier)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(self.free_tier_progress.clone()),
)
.child(
v_flex()
.p_1p5()
.gap_0p5()
.child(
h_flex()
.justify_between()
.child(Label::new("Current Spending").size(LabelSize::Small))
.child(
Label::new(formatted_over_tier)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(self.over_tier_progress.clone()),
),
)
}
}
impl Component for UserSpending {
fn scope() -> ComponentScope {
ComponentScope::None
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let new_user = cx.new(|cx| UserSpending::new(0, 2000, 0, 2000, cx));
let free_capped = cx.new(|cx| UserSpending::new(2000, 2000, 0, 2000, cx));
let free_near_capped = cx.new(|cx| UserSpending::new(1800, 2000, 0, 2000, cx));
let over_near_capped = cx.new(|cx| UserSpending::new(2000, 2000, 1800, 2000, cx));
let over_capped = cx.new(|cx| UserSpending::new(1000, 2000, 2000, 2000, cx));
Some(
v_flex()
.gap_6()
.p_4()
.children(vec![example_group(vec![
single_example(
"New User",
div().size_full().child(new_user.clone()).into_any_element(),
),
single_example(
"Free Tier Capped",
div()
.size_full()
.child(free_capped.clone())
.into_any_element(),
),
single_example(
"Free Tier Near Capped",
div()
.size_full()
.child(free_near_capped.clone())
.into_any_element(),
),
single_example(
"Over Tier Near Capped",
div()
.size_full()
.child(over_near_capped.clone())
.into_any_element(),
),
single_example(
"Over Tier Capped",
div()
.size_full()
.child(over_capped.clone())
.into_any_element(),
),
])])
.into_any_element(),
)
}
}

45
crates/agent2/Cargo.toml Normal file
View File

@@ -0,0 +1,45 @@
[package]
name = "agent2"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
publish = false
[lib]
path = "src/agent2.rs"
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
assistant_tool.workspace = true
assistant_tools.workspace = true
chrono.workspace = true
collections.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
language_model.workspace = true
language_models.workspace = true
parking_lot.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
thiserror.workspace = true
util.workspace = true
[dev-dependencies]
ctor.workspace = true
client = { workspace = true, "features" = ["test-support"] }
env_logger.workspace = true
fs = { workspace = true, "features" = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }
gpui_tokio.workspace = true
language_model = { workspace = true, "features" = ["test-support"] }
project = { workspace = true, "features" = ["test-support"] }
reqwest_client.workspace = true
settings = { workspace = true, "features" = ["test-support"] }

278
crates/agent2/src/agent2.rs Normal file
View File

@@ -0,0 +1,278 @@
#[cfg(test)]
mod tests;
use anyhow::Result;
use assistant_tool::{ActionLog, Tool};
use futures::{channel::mpsc, future};
use gpui::{Context, Entity, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolSchemaFormat,
LanguageModelToolUse, MessageContent, Role, StopReason,
};
use project::Project;
use smol::stream::StreamExt;
use std::{collections::BTreeMap, sync::Arc};
use util::ResultExt;
#[derive(Debug)]
pub struct AgentMessage {
pub role: Role,
pub content: Vec<MessageContent>,
}
impl AgentMessage {
fn to_request_message(&self) -> LanguageModelRequestMessage {
LanguageModelRequestMessage {
role: self.role,
content: self.content.clone(),
cache: false, // TODO: Figure out caching
}
}
}
pub type AgentResponseEvent = LanguageModelCompletionEvent;
pub struct Agent {
messages: Vec<AgentMessage>,
/// Holds the task that handles agent interaction until the end of the turn.
/// Survives across multiple requests as the model performs tool calls and
/// we run tools, report their results.
running_turn: Option<Task<()>>,
tools: BTreeMap<Arc<str>, Arc<dyn Tool>>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
}
impl Agent {
pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
Self {
messages: Vec::new(),
running_turn: None,
tools: BTreeMap::default(),
project,
action_log,
}
}
pub fn add_tool(&mut self, tool: Arc<dyn Tool>) {
let name = Arc::from(tool.name());
self.tools.insert(name, tool);
}
pub fn remove_tool(&mut self, name: &str) -> bool {
self.tools.remove(name).is_some()
}
/// Sending a message results in the model streaming a response, which could include tool calls.
/// After calling tools, the model will stops and waits for any outstanding tool calls to be completed and their results sent.
/// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn.
pub fn send(
&mut self,
model: Arc<dyn LanguageModel>,
content: impl Into<MessageContent>,
cx: &mut Context<Self>,
) -> mpsc::UnboundedReceiver<Result<AgentResponseEvent>> {
cx.notify();
let (events_tx, events_rx) = mpsc::unbounded();
self.messages.push(AgentMessage {
role: Role::User,
content: vec![content.into()],
});
self.running_turn = Some(cx.spawn(async move |thread, cx| {
let turn_result = async {
// Perform one request, then keep looping if the model makes tool calls.
loop {
let request =
thread.update(cx, |thread, _cx| thread.build_completion_request())?;
println!(
"request: {}",
serde_json::to_string_pretty(&request).unwrap()
);
// Stream events, appending to messages and collecting up tool uses.
let mut events = model.stream_completion(request, cx).await?;
let mut tool_uses = Vec::new();
while let Some(event) = events.next().await {
match event {
Ok(event) => {
thread
.update(cx, |thread, cx| {
tool_uses.extend(thread.handle_response_event(
event,
events_tx.clone(),
cx,
));
})
.ok();
}
Err(error) => {
events_tx.unbounded_send(Err(error)).ok();
break;
}
}
}
// If there are no tool uses, the turn is done.
if tool_uses.is_empty() {
break;
}
// If there are tool uses, wait for their results to be
// computed, then send them together in a single message on
// the next loop iteration.
let tool_results = future::join_all(tool_uses).await;
thread
.update(cx, |thread, _cx| {
thread.messages.push(AgentMessage {
role: Role::User,
content: tool_results.into_iter().map(Into::into).collect(),
});
})
.ok();
}
anyhow::Ok(())
}
.await;
if let Err(error) = turn_result {
events_tx.unbounded_send(Err(error)).ok();
}
}));
events_rx
}
fn handle_response_event(
&mut self,
event: LanguageModelCompletionEvent,
events_tx: mpsc::UnboundedSender<Result<AgentResponseEvent>>,
cx: &mut Context<Self>,
) -> Option<Task<LanguageModelToolResult>> {
use LanguageModelCompletionEvent::*;
events_tx.unbounded_send(Ok(event.clone())).ok();
match event {
Text(new_text) => self.handle_text_event(new_text, cx),
Thinking { text, signature } => {}
ToolUse(tool_use) => {
return Some(self.handle_tool_use_event(tool_use, cx));
}
StartMessage { message_id, role } => {
self.messages.push(AgentMessage {
role,
content: Vec::new(),
});
}
UsageUpdate(token_usage) => {}
Stop(stop_reason) => self.handle_stop_event(stop_reason),
}
None
}
fn handle_stop_event(&mut self, stop_reason: StopReason) {
match stop_reason {
StopReason::EndTurn | StopReason::ToolUse => {}
StopReason::MaxTokens => todo!(),
}
}
fn handle_text_event(&mut self, new_text: String, cx: &mut Context<Self>) {
if let Some(last_message) = self.messages.last_mut() {
debug_assert!(last_message.role == Role::Assistant);
if let Some(MessageContent::Text(text)) = last_message.content.last_mut() {
text.push_str(&new_text);
} else {
last_message.content.push(MessageContent::Text(new_text));
}
cx.notify();
} else {
todo!("does this happen in practice?");
}
}
fn handle_tool_use_event(
&mut self,
tool_use: LanguageModelToolUse,
cx: &mut Context<Self>,
) -> Task<LanguageModelToolResult> {
if let Some(last_message) = self.messages.last_mut() {
debug_assert!(last_message.role == Role::Assistant);
last_message.content.push(tool_use.clone().into());
cx.notify();
} else {
todo!("does this happen in practice?");
}
if let Some(tool) = self.tools.get(&tool_use.name) {
let pending_tool_result = tool.clone().run(
tool_use.input,
&self.build_request_messages(),
self.project.clone(),
self.action_log.clone(),
cx,
);
cx.foreground_executor().spawn(async move {
match pending_tool_result.output.await {
Ok(tool_output) => LanguageModelToolResult {
tool_use_id: tool_use.id,
tool_name: tool_use.name,
is_error: false,
content: Arc::from(tool_output),
},
Err(error) => LanguageModelToolResult {
tool_use_id: tool_use.id,
tool_name: tool_use.name,
is_error: true,
content: Arc::from(error.to_string()),
},
}
})
} else {
Task::ready(LanguageModelToolResult {
content: Arc::from(format!("No tool named {} exists", tool_use.name)),
tool_use_id: tool_use.id,
tool_name: tool_use.name,
is_error: true,
})
}
}
fn build_completion_request(&self) -> LanguageModelRequest {
LanguageModelRequest {
thread_id: None,
prompt_id: None,
messages: self.build_request_messages(),
tools: self
.tools
.values()
.filter_map(|tool| {
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema: tool
.input_schema(LanguageModelToolSchemaFormat::JsonSchema)
.log_err()?,
})
})
.collect(),
stop: Vec::new(),
temperature: None,
}
}
fn build_request_messages(&self) -> Vec<LanguageModelRequestMessage> {
self.messages
.iter()
.map(|message| LanguageModelRequestMessage {
role: message.role,
content: message.content.clone(),
cache: false,
})
.collect()
}
}

191
crates/agent2/src/tests.rs Normal file
View File

@@ -0,0 +1,191 @@
use super::*;
use assistant_tool::{IconName, Project, ToolResult};
use client::{Client, UserStore};
use fs::FakeFs;
use gpui::{AppContext, TestAppContext};
use language_model::LanguageModelRegistry;
use reqwest_client::ReqwestClient;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::Duration;
mod tools;
use tools::*;
#[gpui::test]
async fn test_echo(cx: &mut TestAppContext) {
let AgentTest { model, agent, .. } = agent_test(cx).await;
let events = agent
.update(cx, |agent, cx| {
agent.send(model.clone(), "Testing: Reply with 'Hello'", cx)
})
.collect()
.await;
agent.update(cx, |agent, _cx| {
assert_eq!(
agent.messages.last().unwrap().content,
vec![MessageContent::Text("Hello".to_string())]
);
});
assert_eq!(stop_events(events), vec![StopReason::EndTurn]);
}
#[gpui::test]
async fn test_tool_calls(cx: &mut TestAppContext) {
let AgentTest { model, agent, .. } = agent_test(cx).await;
// Test a tool calls that's likely to complete before streaming stops.
let events = agent
.update(cx, |agent, cx| {
agent.add_tool(Arc::new(EchoTool));
agent.send(
model.clone(),
"Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'.",
cx,
)
})
.collect()
.await;
assert_eq!(
stop_events(events),
vec![StopReason::ToolUse, StopReason::EndTurn]
);
// Test a tool calls that's likely to complete after streaming stops.
let events = agent
.update(cx, |agent, cx| {
agent.remove_tool(&EchoTool.name());
agent.add_tool(Arc::new(DelayTool));
agent.send(
model.clone(),
"Now call the delay tool with 200ms. When the timer goes off, then you echo the output of the tool.",
cx,
)
})
.collect()
.await;
assert_eq!(
stop_events(events),
vec![StopReason::ToolUse, StopReason::EndTurn]
);
agent.update(cx, |agent, _cx| {
assert!(agent
.messages
.last()
.unwrap()
.content
.iter()
.any(|content| {
if let MessageContent::Text(text) = content {
text.contains("Ding")
} else {
false
}
}));
});
}
#[gpui::test]
async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
let AgentTest { model, agent, .. } = agent_test(cx).await;
// Test concurrent tool calls with different delay times
let events = agent
.update(cx, |agent, cx| {
agent.add_tool(Arc::new(DelayTool));
agent.send(
model.clone(),
"Call the delay tool twice in the same message. Once with 100ms. Once with 300ms. When both timers are complete, describe the outputs.",
cx,
)
})
.map(|event| dbg!(event))
.collect()
.await;
let stop_reasons = stop_events(events);
assert_eq!(stop_reasons, vec![StopReason::ToolUse, StopReason::EndTurn]);
agent.update(cx, |agent, _cx| {
let last_message = agent.messages.last().unwrap();
let text = last_message
.content
.iter()
.filter_map(|content| {
if let MessageContent::Text(text) = content {
Some(text.as_str())
} else {
None
}
})
.collect::<String>();
assert!(text.contains("Ding"));
});
}
fn stop_events(result_events: Vec<Result<AgentResponseEvent>>) -> Vec<StopReason> {
result_events
.into_iter()
.filter_map(|event| match event.unwrap() {
LanguageModelCompletionEvent::Stop(stop_reason) => Some(stop_reason),
_ => None,
})
.collect()
}
struct AgentTest {
model: Arc<dyn LanguageModel>,
agent: Entity<Agent>,
}
async fn agent_test(cx: &mut TestAppContext) -> AgentTest {
cx.executor().allow_parking();
cx.update(settings::init);
let fs = FakeFs::new(cx.executor().clone());
let project = Project::test(fs.clone(), [], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let agent = cx.new(|_| Agent::new(project.clone(), action_log.clone()));
let model = cx
.update(|cx| {
gpui_tokio::init(cx);
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client));
client::init_settings(cx);
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
let models = LanguageModelRegistry::read_global(cx);
let model = models
.available_models(cx)
.find(|model| model.id().0 == "claude-3-7-sonnet-latest")
.unwrap();
let provider = models.provider(&model.provider_id()).unwrap();
let authenticated = provider.authenticate(cx);
cx.spawn(async move |cx| {
authenticated.await.unwrap();
model
})
})
.await;
AgentTest {
model,
agent: agent,
}
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}

View File

@@ -0,0 +1,102 @@
use super::*;
#[derive(JsonSchema, Serialize, Deserialize)]
pub struct EchoToolInput {
text: String,
}
pub struct EchoTool;
impl Tool for EchoTool {
fn name(&self) -> String {
"echo".to_string()
}
fn description(&self) -> String {
"A tool that echoes its input".to_string()
}
fn icon(&self) -> IconName {
IconName::Ai
}
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &gpui::App) -> bool {
false
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
"Echo".to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_project: gpui::Entity<Project>,
_action_log: gpui::Entity<assistant_tool::ActionLog>,
cx: &mut gpui::App,
) -> ToolResult {
ToolResult {
output: cx.foreground_executor().spawn(async move {
let input: EchoToolInput = serde_json::from_value(input)?;
Ok(input.text)
}),
card: None,
}
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
assistant_tools::json_schema_for::<EchoToolInput>(format)
}
}
#[derive(JsonSchema, Serialize, Deserialize)]
pub struct DelayToolInput {
ms: u64,
}
pub struct DelayTool;
impl Tool for DelayTool {
fn name(&self) -> String {
"delay".to_string()
}
fn description(&self) -> String {
"A tool that waits for a specified delay".to_string()
}
fn icon(&self) -> IconName {
IconName::Cog
}
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &gpui::App) -> bool {
false
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
"Delay".to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_project: gpui::Entity<Project>,
_action_log: gpui::Entity<assistant_tool::ActionLog>,
cx: &mut gpui::App,
) -> ToolResult {
ToolResult {
output: cx.foreground_executor().spawn(async move {
let input: DelayToolInput = serde_json::from_value(input)?;
smol::Timer::after(Duration::from_millis(input.ms)).await;
Ok("Ding".to_string())
}),
card: None,
}
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
assistant_tools::json_schema_for::<DelayToolInput>(format)
}
}

View File

@@ -74,6 +74,10 @@ pub enum Model {
}
impl Model {
pub fn default_fast() -> Self {
Self::Claude3_5Haiku
}
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-3-5-sonnet") {
Ok(Self::Claude3_5Sonnet)
@@ -412,6 +416,7 @@ pub async fn stream_completion_with_rate_limit_info(
let beta_headers = Model::from_id(&request.base.model)
.map(|model| model.beta_headers())
.unwrap_or_else(|_err| Model::DEFAULT_BETA_HEADERS.join(","));
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
@@ -419,6 +424,7 @@ pub async fn stream_completion_with_rate_limit_info(
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
let serialized_request =
serde_json::to_string(&request).context("failed to serialize request")?;
let request = request_builder
@@ -507,6 +513,15 @@ pub enum RequestContent {
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "thinking")]
Thinking {
thinking: String,
signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "redacted_thinking")]
RedactedThinking { data: String },
#[serde(rename = "image")]
Image {
source: ImageSource,

View File

@@ -18,5 +18,4 @@ gpui.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true
which.workspace = true
workspace-hack.workspace = true

View File

@@ -72,6 +72,8 @@ impl AskPassSession {
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let listener =
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
let zed_path = std::env::current_exe()
.context("Failed to figure out current executable path for use in askpass")?;
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
let mut kill_tx = Some(askpass_kill_master_tx);
@@ -110,21 +112,10 @@ impl AskPassSession {
drop(temp_dir)
});
anyhow::ensure!(
which::which("nc").is_ok(),
"Cannot find `nc` command (netcat), which is required to connect over SSH."
);
// Create an askpass script that communicates back to this process.
let askpass_script = format!(
"{shebang}\n{print_args} | {nc} -U {askpass_socket} 2> /dev/null \n",
// on macOS `brew install netcat` provides the GNU netcat implementation
// which does not support -U.
nc = if cfg!(target_os = "macos") {
"/usr/bin/nc"
} else {
"nc"
},
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
zed_exe = zed_path.display(),
askpass_socket = askpass_socket.display(),
print_args = "printf '%s\\0' \"$@\"",
shebang = "#!/bin/sh",
@@ -170,6 +161,51 @@ impl AskPassSession {
}
}
/// The main function for when Zed is running in netcat mode for use in askpass.
/// Called from both the remote server binary and the zed binary in their respective main functions.
#[cfg(unix)]
pub fn main(socket: &str) {
use std::io::{self, Read, Write};
use std::os::unix::net::UnixStream;
use std::process::exit;
let mut stream = match UnixStream::connect(socket) {
Ok(stream) => stream,
Err(err) => {
eprintln!("Error connecting to socket {}: {}", socket, err);
exit(1);
}
};
let mut buffer = Vec::new();
if let Err(err) = io::stdin().read_to_end(&mut buffer) {
eprintln!("Error reading from stdin: {}", err);
exit(1);
}
if buffer.last() != Some(&b'\0') {
buffer.push(b'\0');
}
if let Err(err) = stream.write_all(&buffer) {
eprintln!("Error writing to socket: {}", err);
exit(1);
}
let mut response = Vec::new();
if let Err(err) = stream.read_to_end(&mut response) {
eprintln!("Error reading from socket: {}", err);
exit(1);
}
if let Err(err) = io::stdout().write_all(&response) {
eprintln!("Error writing to stdout: {}", err);
exit(1);
}
}
#[cfg(not(unix))]
pub fn main(_socket: &str) {}
#[cfg(not(unix))]
pub struct AskPassSession {
path: PathBuf,

View File

@@ -8,7 +8,7 @@ mod terminal_inline_assistant;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_settings::{AssistantSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
@@ -161,71 +161,38 @@ fn init_language_model_settings(cx: &mut App) {
fn update_active_language_model_from_settings(cx: &mut App) {
let settings = AssistantSettings::get_global(cx);
// Default model - used as fallback
let active_model_provider_name =
LanguageModelProviderId::from(settings.default_model.provider.clone());
let active_model_id = LanguageModelId::from(settings.default_model.model.clone());
// Inline assistant model
let inline_assistant_model = settings
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
language_model::SelectedModel {
provider: LanguageModelProviderId::from(selection.provider.clone()),
model: LanguageModelId::from(selection.model.clone()),
}
}
let default = to_selected_model(&settings.default_model);
let inline_assistant = settings
.inline_assistant_model
.as_ref()
.unwrap_or(&settings.default_model);
let inline_assistant_provider_name =
LanguageModelProviderId::from(inline_assistant_model.provider.clone());
let inline_assistant_model_id = LanguageModelId::from(inline_assistant_model.model.clone());
// Commit message model
let commit_message_model = settings
.map(to_selected_model);
let commit_message = settings
.commit_message_model
.as_ref()
.unwrap_or(&settings.default_model);
let commit_message_provider_name =
LanguageModelProviderId::from(commit_message_model.provider.clone());
let commit_message_model_id = LanguageModelId::from(commit_message_model.model.clone());
// Thread summary model
let thread_summary_model = settings
.map(to_selected_model);
let thread_summary = settings
.thread_summary_model
.as_ref()
.unwrap_or(&settings.default_model);
let thread_summary_provider_name =
LanguageModelProviderId::from(thread_summary_model.provider.clone());
let thread_summary_model_id = LanguageModelId::from(thread_summary_model.model.clone());
.map(to_selected_model);
let inline_alternatives = settings
.inline_alternatives
.iter()
.map(|alternative| {
(
LanguageModelProviderId::from(alternative.provider.clone()),
LanguageModelId::from(alternative.model.clone()),
)
})
.map(to_selected_model)
.collect::<Vec<_>>();
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
// Set the default model
registry.select_default_model(&active_model_provider_name, &active_model_id, cx);
// Set the specific models
registry.select_inline_assistant_model(
&inline_assistant_provider_name,
&inline_assistant_model_id,
cx,
);
registry.select_commit_message_model(
&commit_message_provider_name,
&commit_message_model_id,
cx,
);
registry.select_thread_summary_model(
&thread_summary_provider_name,
&thread_summary_model_id,
cx,
);
// Set the alternatives
registry.select_default_model(Some(&default), cx);
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
registry.select_commit_message_model(commit_message.as_ref(), cx);
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
registry.select_inline_alternative_models(inline_alternatives, cx);
});
}

View File

@@ -13,7 +13,7 @@ use assistant_context_editor::{
use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use client::{Client, Status, proto};
use editor::{Editor, EditorEvent};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
@@ -27,10 +27,13 @@ use language_model::{
};
use project::Project;
use prompt_library::{PromptLibrary, open_prompt_library};
use prompt_store::PromptBuilder;
use prompt_store::{PromptBuilder, PromptId};
use search::{BufferSearchBar, buffer_search::DivRegistrar};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
use std::ops::Range;
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
@@ -59,7 +62,7 @@ pub fn init(cx: &mut App) {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
panel.deploy_prompt_library(&OpenPromptLibrary::default(), window, cx)
});
}
});
@@ -269,7 +272,10 @@ impl AssistantPanel {
menu.context(focus_handle.clone())
.action("New Chat", Box::new(NewChat))
.action("History", Box::new(DeployHistory))
.action("Prompt Library", Box::new(OpenPromptLibrary))
.action(
"Prompt Library",
Box::new(OpenPromptLibrary::default()),
)
.action("Configure", Box::new(ShowConfiguration))
.action(zoom_label, Box::new(ToggleZoom))
}))
@@ -1040,7 +1046,7 @@ impl AssistantPanel {
fn deploy_prompt_library(
&mut self,
_: &OpenPromptLibrary,
action: &OpenPromptLibrary,
_window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1054,6 +1060,7 @@ impl AssistantPanel {
None,
))
}),
action.prompt_to_focus.map(|uuid| PromptId::User { uuid }),
cx,
)
.detach_and_log_err(cx);
@@ -1413,7 +1420,8 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn quote_selection(
&self,
workspace: &mut Workspace,
creases: Vec<(String, String)>,
selection_ranges: Vec<Range<Anchor>>,
buffer: Entity<MultiBuffer>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
@@ -1425,6 +1433,12 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
}
let snapshot = buffer.read(cx).snapshot(cx);
let selection_ranges = selection_ranges
.into_iter()
.map(|range| range.to_point(&snapshot))
.collect::<Vec<_>>();
panel.update(cx, |_, cx| {
// Wait to create a new context until the workspace is no longer
// being updated.
@@ -1433,7 +1447,9 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
.active_context_editor(cx)
.or_else(|| panel.new_context(window, cx))
{
context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
context.update(cx, |context, cx| {
context.quote_ranges(selection_ranges, snapshot, window, cx)
});
};
});
});

View File

@@ -2978,6 +2978,8 @@ impl CodegenAlternative {
});
Ok(LanguageModelRequest {
thread_id: None,
prompt_id: None,
messages,
tools: Vec::new(),
stop: Vec::new(),

View File

@@ -292,6 +292,8 @@ impl TerminalInlineAssistant {
});
Ok(LanguageModelRequest {
thread_id: None,
prompt_id: None,
messages,
tools: Vec::new(),
stop: Vec::new(),

View File

@@ -2373,7 +2373,7 @@ impl AssistantContext {
LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason;
}
LanguageModelCompletionEvent::Thinking(chunk) => {
LanguageModelCompletionEvent::Thinking { text: chunk, .. } => {
if thought_process_stack.is_empty() {
let start =
buffer.anchor_before(message_old_end_offset);
@@ -2555,6 +2555,8 @@ impl AssistantContext {
}
let mut completion_request = LanguageModelRequest {
thread_id: None,
prompt_id: None,
messages: Vec::new(),
tools: Vec::new(),
stop: Vec::new(),

View File

@@ -8,8 +8,8 @@ use assistant_slash_commands::{
use client::{proto, zed_urls};
use collections::{BTreeSet, HashMap, HashSet, hash_map};
use editor::{
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot,
ProposedChangeLocation, ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
actions::{MoveToEndOfLine, Newline, ShowCompletions},
display_map::{
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
@@ -155,7 +155,8 @@ pub trait AssistantPanelDelegate {
fn quote_selection(
&self,
workspace: &mut Workspace,
creases: Vec<(String, String)>,
selection_ranges: Vec<Range<Anchor>>,
buffer: Entity<MultiBuffer>,
window: &mut Window,
cx: &mut Context<Workspace>,
);
@@ -1800,23 +1801,45 @@ impl ContextEditor {
return;
};
let Some(creases) = selections_creases(workspace, cx) else {
let Some((selections, buffer)) = maybe!({
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))?;
let buffer = editor.read(cx).buffer().clone();
let snapshot = buffer.read(cx).snapshot(cx);
let selections = editor.update(cx, |editor, cx| {
editor
.selections
.all_adjusted(cx)
.into_iter()
.filter_map(|s| {
(!s.is_empty())
.then(|| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
})
.collect::<Vec<_>>()
});
Some((selections, buffer))
}) else {
return;
};
if creases.is_empty() {
if selections.is_empty() {
return;
}
assistant_panel_delegate.quote_selection(workspace, creases, window, cx);
assistant_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
}
pub fn quote_creases(
pub fn quote_ranges(
&mut self,
creases: Vec<(String, String)>,
ranges: Vec<Range<Point>>,
snapshot: MultiBufferSnapshot,
window: &mut Window,
cx: &mut Context<Self>,
) {
let creases = selections_creases(ranges, snapshot, cx);
self.editor.update(cx, |editor, cx| {
editor.insert("\n", window, cx);
for (text, crease_title) in creases {

View File

@@ -54,9 +54,9 @@ impl SlashCommand for DefaultSlashCommand {
cx: &mut App,
) -> Task<SlashCommandResult> {
let store = PromptStore::global(cx);
cx.background_spawn(async move {
cx.spawn(async move |cx| {
let store = store.await?;
let prompts = store.default_prompt_metadata();
let prompts = store.read_with(cx, |store, _cx| store.default_prompt_metadata())?;
let mut text = String::new();
text.push('\n');

View File

@@ -5,7 +5,7 @@ use assistant_slash_command::{
};
use gpui::{Task, WeakEntity};
use language::{BufferSnapshot, LspAdapterDelegate};
use prompt_store::PromptStore;
use prompt_store::{PromptMetadata, PromptStore};
use std::sync::{Arc, atomic::AtomicBool};
use ui::prelude::*;
use workspace::Workspace;
@@ -43,8 +43,11 @@ impl SlashCommand for PromptSlashCommand {
) -> Task<Result<Vec<ArgumentCompletion>>> {
let store = PromptStore::global(cx);
let query = arguments.to_owned().join(" ");
cx.background_spawn(async move {
let prompts = store.await?.search(query).await;
cx.spawn(async move |cx| {
let prompts: Vec<PromptMetadata> = store
.await?
.read_with(cx, |store, cx| store.search(query, cx))?
.await;
Ok(prompts
.into_iter()
.filter_map(|prompt| {
@@ -77,14 +80,18 @@ impl SlashCommand for PromptSlashCommand {
let store = PromptStore::global(cx);
let title = SharedString::from(title.clone());
let prompt = cx.background_spawn({
let prompt = cx.spawn({
let title = title.clone();
async move {
async move |cx| {
let store = store.await?;
let prompt_id = store
.id_for_title(&title)
.with_context(|| format!("no prompt found with title {:?}", title))?;
let body = store.load(prompt_id).await?;
let body = store
.read_with(cx, |store, cx| {
let prompt_id = store
.id_for_title(&title)
.with_context(|| format!("no prompt found with title {:?}", title))?;
anyhow::Ok(store.load(prompt_id, cx))
})??
.await?;
anyhow::Ok(body)
}
});

View File

@@ -3,10 +3,12 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
SlashCommandOutputSection, SlashCommandResult,
};
use editor::Editor;
use editor::{Editor, MultiBufferSnapshot};
use futures::StreamExt;
use gpui::{App, Context, SharedString, Task, WeakEntity, Window};
use gpui::{App, SharedString, Task, WeakEntity, Window};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use rope::Point;
use std::ops::Range;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use ui::IconName;
@@ -69,7 +71,22 @@ impl SlashCommand for SelectionCommand {
let mut events = vec![];
let Some(creases) = workspace
.update(cx, selections_creases)
.update(cx, |workspace, cx| {
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))?;
editor.update(cx, |editor, cx| {
let selection_ranges = editor
.selections
.all_adjusted(cx)
.iter()
.map(|selection| selection.range())
.collect::<Vec<_>>();
let snapshot = editor.buffer().read(cx).snapshot(cx);
Some(selections_creases(selection_ranges, snapshot, cx))
})
})
.unwrap_or_else(|e| {
events.push(Err(e));
None
@@ -102,94 +119,82 @@ impl SlashCommand for SelectionCommand {
}
pub fn selections_creases(
workspace: &mut workspace::Workspace,
cx: &mut Context<Workspace>,
) -> Option<Vec<(String, String)>> {
let editor = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))?;
let mut creases = vec![];
editor.update(cx, |editor, cx| {
let selections = editor.selections.all_adjusted(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
for selection in selections {
let range = editor::ToOffset::to_offset(&selection.start, &buffer)
..editor::ToOffset::to_offset(&selection.end, &buffer);
let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
if selected_text.is_empty() {
continue;
}
let start_language = buffer.language_at(range.start);
let end_language = buffer.language_at(range.end);
let language_name = if start_language == end_language {
start_language.map(|language| language.code_fence_block_name())
} else {
None
};
let language_name = language_name.as_deref().unwrap_or("");
let filename = buffer
.file_at(selection.start)
.map(|file| file.full_path(cx));
let text = if language_name == "markdown" {
selected_text
.lines()
.map(|line| format!("> {}", line))
.collect::<Vec<_>>()
.join("\n")
} else {
let start_symbols = buffer
.symbols_containing(selection.start, None)
.map(|(_, symbols)| symbols);
let end_symbols = buffer
.symbols_containing(selection.end, None)
.map(|(_, symbols)| symbols);
let outline_text =
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
Some(
start_symbols
.into_iter()
.zip(end_symbols)
.take_while(|(a, b)| a == b)
.map(|(a, _)| a.text)
.collect::<Vec<_>>()
.join(" > "),
)
} else {
None
};
let line_comment_prefix = start_language
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
let fence = codeblock_fence_for_path(
filename.as_deref(),
Some(selection.start.row..=selection.end.row),
);
if let Some((line_comment_prefix, outline_text)) =
line_comment_prefix.zip(outline_text)
{
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
format!("{fence}{breadcrumb}{selected_text}\n```")
} else {
format!("{fence}{selected_text}\n```")
}
};
let crease_title = if let Some(path) = filename {
let start_line = selection.start.row + 1;
let end_line = selection.end.row + 1;
if start_line == end_line {
format!("{}, Line {}", path.display(), start_line)
} else {
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
}
} else {
"Quoted selection".to_string()
};
creases.push((text, crease_title));
selection_ranges: Vec<Range<Point>>,
snapshot: MultiBufferSnapshot,
cx: &App,
) -> Vec<(String, String)> {
let mut creases = Vec::new();
for range in selection_ranges {
let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
if selected_text.is_empty() {
continue;
}
});
Some(creases)
let start_language = snapshot.language_at(range.start);
let end_language = snapshot.language_at(range.end);
let language_name = if start_language == end_language {
start_language.map(|language| language.code_fence_block_name())
} else {
None
};
let language_name = language_name.as_deref().unwrap_or("");
let filename = snapshot.file_at(range.start).map(|file| file.full_path(cx));
let text = if language_name == "markdown" {
selected_text
.lines()
.map(|line| format!("> {}", line))
.collect::<Vec<_>>()
.join("\n")
} else {
let start_symbols = snapshot
.symbols_containing(range.start, None)
.map(|(_, symbols)| symbols);
let end_symbols = snapshot
.symbols_containing(range.end, None)
.map(|(_, symbols)| symbols);
let outline_text =
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
Some(
start_symbols
.into_iter()
.zip(end_symbols)
.take_while(|(a, b)| a == b)
.map(|(a, _)| a.text)
.collect::<Vec<_>>()
.join(" > "),
)
} else {
None
};
let line_comment_prefix = start_language
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
let fence = codeblock_fence_for_path(
filename.as_deref(),
Some(range.start.row..=range.end.row),
);
if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text)
{
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
format!("{fence}{breadcrumb}{selected_text}\n```")
} else {
format!("{fence}{selected_text}\n```")
}
};
let crease_title = if let Some(path) = filename {
let start_line = range.start.row + 1;
let end_line = range.end.row + 1;
if start_line == end_line {
format!("{}, Line {}", path.display(), start_line)
} else {
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
}
} else {
"Quoted selection".to_string()
};
creases.push((text, crease_title));
}
creases
}

View File

@@ -3,7 +3,7 @@ use buffer_diff::BufferDiff;
use collections::BTreeMap;
use futures::{StreamExt, channel::mpsc};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
use language::{Anchor, Buffer, BufferEvent, DiskState, Point};
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
use text::{Edit, Patch, Rope};
@@ -363,10 +363,10 @@ impl ActionLog {
}
}
pub fn reject_edits_in_range(
pub fn reject_edits_in_ranges(
&mut self,
buffer: Entity<Buffer>,
buffer_range: Range<impl language::ToPoint>,
buffer_ranges: Vec<Range<impl language::ToPoint>>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
@@ -403,29 +403,15 @@ impl ActionLog {
}
TrackedBufferStatus::Modified => {
buffer.update(cx, |buffer, cx| {
let buffer_range =
buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
let mut buffer_row_ranges = buffer_ranges
.into_iter()
.map(|range| {
range.start.to_point(buffer).row..range.end.to_point(buffer).row
})
.peekable();
let mut edits_to_revert = Vec::new();
for edit in tracked_buffer.unreviewed_changes.edits() {
if buffer_range.end.row < edit.new.start {
break;
} else if buffer_range.start.row > edit.new.end {
continue;
}
let old_range = tracked_buffer
.base_text
.point_to_offset(Point::new(edit.old.start, 0))
..tracked_buffer.base_text.point_to_offset(cmp::min(
Point::new(edit.old.end, 0),
tracked_buffer.base_text.max_point(),
));
let old_text = tracked_buffer
.base_text
.chunks_in_range(old_range)
.collect::<String>();
let new_range = tracked_buffer
.snapshot
.anchor_before(Point::new(edit.new.start, 0))
@@ -433,7 +419,35 @@ impl ActionLog {
Point::new(edit.new.end, 0),
tracked_buffer.snapshot.max_point(),
));
edits_to_revert.push((new_range, old_text));
let new_row_range = new_range.start.to_point(buffer).row
..new_range.end.to_point(buffer).row;
let mut revert = false;
while let Some(buffer_row_range) = buffer_row_ranges.peek() {
if buffer_row_range.end < new_row_range.start {
buffer_row_ranges.next();
} else if buffer_row_range.start > new_row_range.end {
break;
} else {
revert = true;
break;
}
}
if revert {
let old_range = tracked_buffer
.base_text
.point_to_offset(Point::new(edit.old.start, 0))
..tracked_buffer.base_text.point_to_offset(cmp::min(
Point::new(edit.old.end, 0),
tracked_buffer.base_text.max_point(),
));
let old_text = tracked_buffer
.base_text
.chunks_in_range(old_range)
.collect::<String>();
edits_to_revert.push((new_range, old_text));
}
}
buffer.edit(edits_to_revert, None, cx);
@@ -599,6 +613,7 @@ fn point_to_row_edit(edit: Edit<Point>, old_text: &Rope, new_text: &Rope) -> Edi
}
}
#[derive(Copy, Clone, Debug)]
enum ChangeAuthor {
User,
Agent,
@@ -1135,9 +1150,48 @@ mod tests {
)]
);
// If the rejected range doesn't overlap with any hunk, we ignore it.
action_log
.update(cx, |log, cx| {
log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
log.reject_edits_in_ranges(
buffer.clone(),
vec![Point::new(4, 0)..Point::new(4, 0)],
cx,
)
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
"abc\ndE\nXYZf\nghi\njkl\nmnO"
);
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![
HunkStatus {
range: Point::new(1, 0)..Point::new(3, 0),
diff_status: DiffHunkStatusKind::Modified,
old_text: "def\n".into(),
},
HunkStatus {
range: Point::new(5, 0)..Point::new(5, 3),
diff_status: DiffHunkStatusKind::Modified,
old_text: "mno".into(),
}
],
)]
);
action_log
.update(cx, |log, cx| {
log.reject_edits_in_ranges(
buffer.clone(),
vec![Point::new(0, 0)..Point::new(1, 0)],
cx,
)
})
.await
.unwrap();
@@ -1160,7 +1214,11 @@ mod tests {
action_log
.update(cx, |log, cx| {
log.reject_edits_in_range(buffer.clone(), Point::new(4, 0)..Point::new(4, 0), cx)
log.reject_edits_in_ranges(
buffer.clone(),
vec![Point::new(4, 0)..Point::new(4, 0)],
cx,
)
})
.await
.unwrap();
@@ -1172,6 +1230,82 @@ mod tests {
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
#[gpui::test(iterations = 10)]
async fn test_reject_multiple_edits(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"}))
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx)
.unwrap()
});
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
"abc\ndE\nXYZf\nghi\njkl\nmnO"
);
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![
HunkStatus {
range: Point::new(1, 0)..Point::new(3, 0),
diff_status: DiffHunkStatusKind::Modified,
old_text: "def\n".into(),
},
HunkStatus {
range: Point::new(5, 0)..Point::new(5, 3),
diff_status: DiffHunkStatusKind::Modified,
old_text: "mno".into(),
}
],
)]
);
action_log.update(cx, |log, cx| {
let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0))
..buffer.read(cx).anchor_before(Point::new(1, 0));
let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0))
..buffer.read(cx).anchor_before(Point::new(5, 3));
log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx)
.detach();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
"abc\ndef\nghi\njkl\nmno"
);
});
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
"abc\ndef\nghi\njkl\nmno"
);
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
#[gpui::test(iterations = 10)]
async fn test_reject_deleted_file(cx: &mut TestAppContext) {
init_test(cx);
@@ -1215,7 +1349,11 @@ mod tests {
action_log
.update(cx, |log, cx| {
log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(0, 0), cx)
log.reject_edits_in_ranges(
buffer.clone(),
vec![Point::new(0, 0)..Point::new(0, 0)],
cx,
)
})
.await
.unwrap();
@@ -1266,7 +1404,11 @@ mod tests {
action_log
.update(cx, |log, cx| {
log.reject_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(0, 11), cx)
log.reject_edits_in_ranges(
buffer.clone(),
vec![Point::new(0, 0)..Point::new(0, 11)],
cx,
)
})
.await
.unwrap();
@@ -1312,7 +1454,7 @@ mod tests {
.update(cx, |log, cx| {
let range = buffer.read(cx).random_byte_range(0, &mut rng);
log::info!("rejecting edits in range {:?}", range);
log.reject_edits_in_range(buffer.clone(), range, cx)
log.reject_edits_in_ranges(buffer.clone(), vec![range], cx)
})
.await
.unwrap();

View File

@@ -9,11 +9,15 @@ use std::fmt::Formatter;
use std::sync::Arc;
use anyhow::Result;
use gpui::AnyElement;
use gpui::Context;
use gpui::IntoElement;
use gpui::Window;
use gpui::{App, Entity, SharedString, Task};
use icons::IconName;
pub use icons::IconName;
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
pub use project::Project;
pub use crate::action_log::*;
pub use crate::tool_registry::*;
@@ -24,16 +28,87 @@ pub fn init(cx: &mut App) {
ToolRegistry::default_global(cx);
}
/// The result of running a tool
#[derive(Debug, Clone)]
pub enum ToolUseStatus {
NeedsConfirmation,
Pending,
Running,
Finished(SharedString),
Error(SharedString),
}
impl ToolUseStatus {
pub fn text(&self) -> SharedString {
match self {
ToolUseStatus::NeedsConfirmation => "".into(),
ToolUseStatus::Pending => "".into(),
ToolUseStatus::Running => "".into(),
ToolUseStatus::Finished(out) => out.clone(),
ToolUseStatus::Error(out) => out.clone(),
}
}
}
/// The result of running a tool, containing both the asynchronous output
/// and an optional card view that can be rendered immediately.
pub struct ToolResult {
/// The asynchronous task that will eventually resolve to the tool's output
pub output: Task<Result<String>>,
/// An optional view to present the output of the tool.
pub card: Option<AnyToolCard>,
}
pub trait ToolCard: 'static + Sized {
fn render(
&mut self,
status: &ToolUseStatus,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement;
}
#[derive(Clone)]
pub struct AnyToolCard {
entity: gpui::AnyEntity,
render: fn(
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
cx: &mut App,
) -> AnyElement,
}
impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
fn from(entity: Entity<T>) -> Self {
fn downcast_render<T: ToolCard>(
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let entity = entity.downcast::<T>().unwrap();
entity.update(cx, |entity, cx| {
entity.render(status, window, cx).into_any_element()
})
}
Self {
entity: entity.into(),
render: downcast_render::<T>,
}
}
}
impl AnyToolCard {
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
(self.render)(self.entity.clone(), status, window, cx)
}
}
impl From<Task<Result<String>>> for ToolResult {
/// Convert from a task to a ToolResult
/// Convert from a task to a ToolResult with no card
fn from(output: Task<Result<String>>) -> Self {
Self { output }
Self { output, card: None }
}
}

View File

@@ -16,13 +16,18 @@ anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
collections.workspace = true
component.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
linkme.workspace = true
open.workspace = true
project.workspace = true
regex.workspace = true
schemars.workspace = true
@@ -30,9 +35,10 @@ serde.workspace = true
serde_json.workspace = true
ui.workspace = true
util.workspace = true
worktree.workspace = true
open = { workspace = true }
web_search.workspace = true
workspace-hack.workspace = true
worktree.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }
@@ -40,5 +46,8 @@ gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
pretty_assertions.workspace = true
settings = { workspace = true, features = ["test-support"] }
tree-sitter-rust.workspace = true
workspace = { workspace = true, features = ["test-support"] }
unindent.workspace = true

View File

@@ -7,29 +7,32 @@ mod create_directory_tool;
mod create_file_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
mod fetch_tool;
mod find_replace_file_tool;
mod grep_tool;
mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod open_tool;
mod path_search_tool;
mod read_file_tool;
mod regex_search_tool;
mod rename_tool;
mod replace;
mod schema;
mod symbol_info_tool;
mod terminal_tool;
mod thinking_tool;
mod web_search_tool;
use std::sync::Arc;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use feature_flags::FeatureFlagAppExt;
use gpui::App;
use http_client::HttpClientWithUrl;
use move_path_tool::MovePathTool;
use web_search_tool::WebSearchTool;
use crate::batch_tool::BatchTool;
use crate::code_action_tool::CodeActionTool;
@@ -39,18 +42,19 @@ use crate::create_directory_tool::CreateDirectoryTool;
use crate::create_file_tool::CreateFileTool;
use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_file_tool::EditFileTool;
use crate::fetch_tool::FetchTool;
use crate::find_replace_file_tool::FindReplaceFileTool;
use crate::grep_tool::GrepTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::open_tool::OpenTool;
use crate::path_search_tool::PathSearchTool;
use crate::read_file_tool::ReadFileTool;
use crate::regex_search_tool::RegexSearchTool;
use crate::rename_tool::RenameTool;
use crate::symbol_info_tool::SymbolInfoTool;
use crate::terminal_tool::TerminalTool;
use crate::thinking_tool::ThinkingTool;
pub use schema::json_schema_for;
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
assistant_tool::init(cx);
@@ -62,7 +66,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(CreateFileTool);
registry.register_tool(CopyPathTool);
registry.register_tool(DeletePathTool);
registry.register_tool(FindReplaceFileTool);
registry.register_tool(EditFileTool);
registry.register_tool(SymbolInfoTool);
registry.register_tool(CodeActionTool);
registry.register_tool(MovePathTool);
@@ -74,10 +78,21 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(ContentsTool);
registry.register_tool(PathSearchTool);
registry.register_tool(ReadFileTool);
registry.register_tool(RegexSearchTool);
registry.register_tool(GrepTool);
registry.register_tool(RenameTool);
registry.register_tool(ThinkingTool);
registry.register_tool(FetchTool::new(http_client));
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
move |is_enabled, cx| {
if is_enabled {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
}
}
})
.detach();
}
#[cfg(test)]

View File

@@ -43,7 +43,7 @@ pub struct BatchToolInput {
/// }
/// },
/// {
/// "name": "regex_search",
/// "name": "grep",
/// "input": {
/// "regex": "fn run\\("
/// }
@@ -91,7 +91,7 @@ pub struct BatchToolInput {
/// {
/// "invocations": [
/// {
/// "name": "regex_search",
/// "name": "grep",
/// "input": {
/// "regex": "impl Database"
/// }

View File

@@ -147,7 +147,7 @@ impl Tool for CodeSymbolsTool {
};
cx.spawn(async move |cx| match input.path {
Some(path) => file_outline(project, path, action_log, regex, input.offset, cx).await,
Some(path) => file_outline(project, path, action_log, regex, cx).await,
None => project_symbols(project, regex, input.offset, cx).await,
})
.into()
@@ -159,7 +159,6 @@ pub async fn file_outline(
path: String,
action_log: Entity<ActionLog>,
regex: Option<Regex>,
offset: u32,
cx: &mut AsyncApp,
) -> anyhow::Result<String> {
let buffer = {
@@ -195,7 +194,8 @@ pub async fn file_outline(
.into_iter()
.map(|item| item.to_point(&snapshot)),
regex,
offset,
0,
usize::MAX,
)
.await
}
@@ -294,11 +294,10 @@ async fn project_symbols(
async fn render_outline(
items: impl IntoIterator<Item = OutlineItem<Point>>,
regex: Option<Regex>,
offset: u32,
offset: usize,
results_per_page: usize,
) -> Result<String> {
const RESULTS_PER_PAGE_USIZE: usize = RESULTS_PER_PAGE as usize;
let mut items = items.into_iter().skip(offset as usize);
let mut items = items.into_iter().skip(offset);
let entries = items
.by_ref()
@@ -307,7 +306,7 @@ async fn render_outline(
.as_ref()
.is_none_or(|regex| regex.is_match(&item.text))
})
.take(RESULTS_PER_PAGE_USIZE)
.take(results_per_page)
.collect::<Vec<_>>();
let has_more = items.next().is_some();
@@ -338,7 +337,10 @@ async fn render_outline(
Ok(output)
}
fn render_entries(output: &mut String, items: impl IntoIterator<Item = OutlineItem<Point>>) -> u32 {
fn render_entries(
output: &mut String,
items: impl IntoIterator<Item = OutlineItem<Point>>,
) -> usize {
let mut entries_rendered = 0;
for item in items {

View File

@@ -228,7 +228,7 @@ impl Tool for ContentsTool {
} else {
// File is too big, so return its outline and a suggestion to
// read again with a line number range specified.
let outline = file_outline(project, file_path, action_log, None, 0, cx).await?;
let outline = file_outline(project, file_path, action_log, None, cx).await?;
Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start and end fields to see the implementations of symbols in the outline."))
}

View File

@@ -15,6 +15,7 @@ To get a project-wide diagnostic summary:
{}
</example>
IMPORTANT: When you're done making changes, you **MUST** get the **project** diagnostics (input: `{}`) at the end of your edits so you can fix any problems you might have introduced. **DO NOT** tell the user you're done before doing this!
You may only attempt to fix these up to 3 times. If you have tried 3 times to fix them, and there are still problems remaining, you must not continue trying to fix them, and must instead tell the user that there are problems remaining - and ask if the user would like you to attempt to solve them further.
<guidelines>
- If you think you can fix a diagnostic, make 1-2 attempts and then give up.
- Don't remove code you've generated just because you can't fix an error. The user can help you fix it.
</guidelines>

View File

@@ -0,0 +1,183 @@
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use ui::IconName;
use crate::replace::replace_exact;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
/// The full path of the file to modify in the project.
///
/// WARNING: When specifying which file path need changing, you MUST
/// start each path with one of the project's root directories.
///
/// The following examples assume we have two root directories in the project:
/// - backend
/// - frontend
///
/// <example>
/// `backend/src/main.rs`
///
/// Notice how the file path starts with root-1. Without that, the path
/// would be ambiguous and the call would fail!
/// </example>
///
/// <example>
/// `frontend/db.js`
/// </example>
pub path: PathBuf,
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
pub display_description: String,
/// The text to replace.
pub old_string: String,
/// The text to replace it with.
pub new_string: String,
}
pub struct EditFileTool;
impl Tool for EditFileTool {
fn name(&self) -> String {
"edit_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("edit_file_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::Pencil
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<EditFileToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<EditFileToolInput>(input.clone()) {
Ok(input) => input.display_description,
Err(_) => "Edit file".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<EditFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
cx.spawn(async move |cx: &mut AsyncApp| {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
.context("Path not found in project")
})??;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
if input.old_string.is_empty() {
return Err(anyhow!("`old_string` cannot be empty. Use a different tool if you want to create a file."));
}
if input.old_string == input.new_string {
return Err(anyhow!("The `old_string` and `new_string` are identical, so no changes would be made."));
}
let result = cx
.background_spawn(async move {
// Try to match exactly
let diff = replace_exact(&input.old_string, &input.new_string, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&input.old_string, &input.new_string, &snapshot))?;
if diff.edits.is_empty() {
return None;
}
let old_text = snapshot.text();
Some((old_text, diff))
})
.await;
let Some((old_text, diff)) = result else {
let err = buffer.read_with(cx, |buffer, _cx| {
let file_exists = buffer
.file()
.map_or(false, |file| file.disk_state().exists());
if !file_exists {
anyhow!("{} does not exist", input.path.display())
} else if buffer.is_empty() {
anyhow!(
"{} is empty, so the provided `old_string` wasn't found.",
input.path.display()
)
} else {
anyhow!("Failed to match the provided `old_string`")
}
})?;
return Err(err)
};
let snapshot = cx.update(|cx| {
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx)
});
let snapshot = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.apply_diff(diff, cx);
buffer.finalize_last_transaction();
buffer.snapshot()
});
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
});
snapshot
})?;
project.update( cx, |project, cx| {
project.save_buffer(buffer, cx)
})?.await?;
let diff_str = cx.background_spawn(async move {
let new_text = snapshot.text();
language::unified_diff(&old_text, &new_text)
}).await;
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
}).into()
}
}

View File

@@ -0,0 +1,45 @@
This is a tool for editing files. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. For larger edits, use the `create_file` tool to overwrite files.
Before using this tool:
1. Use the `read_file` tool to understand the file's contents and context
2. Verify the directory path is correct (only applicable when creating new files):
- Use the `list_directory` tool to verify the parent directory exists and is the correct location
To make a file edit, provide the following:
1. path: The full path to the file you wish to modify in the project. This path must include the root directory in the project.
2. old_string: The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation)
3. new_string: The edited text, which will replace the old_string in the file.
The tool will replace ONE occurrence of old_string with new_string in the specified file.
CRITICAL REQUIREMENTS FOR USING THIS TOOL:
1. UNIQUENESS: The old_string MUST uniquely identify the specific instance you want to change. This means:
- Include AT LEAST 3-5 lines of context BEFORE the change point
- Include AT LEAST 3-5 lines of context AFTER the change point
- Include all whitespace, indentation, and surrounding code exactly as it appears in the file
2. SINGLE INSTANCE: This tool can only change ONE instance at a time. If you need to change multiple instances:
- Make separate calls to this tool for each instance
- Each call must uniquely identify its specific instance using extensive context
3. VERIFICATION: Before using this tool:
- Check how many instances of the target text exist in the file
- If multiple instances exist, gather enough context to uniquely identify each one
- Plan separate tool calls for each instance
WARNING: If you do not follow these requirements:
- The tool will fail if old_string matches multiple locations
- The tool will fail if old_string doesn't match exactly (including whitespace)
- You may change the wrong instance if you don't include enough context
When making edits:
- Ensure the edit results in idiomatic, correct code
- Do not leave the code in a broken state
- Always use fully-qualified project paths (starting with the name of one of the project's root directories)
If you want to create a new file, use the `create_file` tool instead of this tool. Don't pass an empty `old_string`.
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.

View File

@@ -0,0 +1,424 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::StreamExt;
use gpui::{App, Entity, Task};
use language::OffsetRangeExt;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{
Project,
search::{SearchQuery, SearchResult},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, fmt::Write, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
use util::paths::PathMatcher;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GrepToolInput {
/// A regex pattern to search for in the entire project. Note that the regex
/// will be parsed by the Rust `regex` crate.
pub regex: String,
/// A glob pattern for the paths of files to include in the search.
/// Supports standard glob patterns like "**/*.rs" or "src/**/*.ts".
/// If omitted, all files in the project will be searched.
pub include_pattern: Option<String>,
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
pub offset: u32,
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
#[serde(default)]
pub case_sensitive: bool,
}
impl GrepToolInput {
/// Which page of search results this is.
pub fn page(&self) -> u32 {
1 + (self.offset / RESULTS_PER_PAGE)
}
}
const RESULTS_PER_PAGE: u32 = 20;
pub struct GrepTool;
impl Tool for GrepTool {
fn name(&self) -> String {
"grep".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./grep_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Regex
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<GrepToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<GrepToolInput>(input.clone()) {
Ok(input) => {
let page = input.page();
let regex_str = MarkdownString::inline_code(&input.regex);
let case_info = if input.case_sensitive {
" (case-sensitive)"
} else {
""
};
if page > 1 {
format!("Get page {page} of search results for regex {regex_str}{case_info}")
} else {
format!("Search files for regex {regex_str}{case_info}")
}
}
Err(_) => "Search with regex".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
const CONTEXT_LINES: u32 = 2;
let input = match serde_json::from_value::<GrepToolInput>(input) {
Ok(input) => input,
Err(error) => {
return Task::ready(Err(anyhow!("Failed to parse input: {}", error))).into();
}
};
let include_matcher = match PathMatcher::new(
input
.include_pattern
.as_ref()
.into_iter()
.collect::<Vec<_>>(),
) {
Ok(matcher) => matcher,
Err(error) => {
return Task::ready(Err(anyhow!("invalid include glob pattern: {}", error))).into();
}
};
let query = match SearchQuery::regex(
&input.regex,
false,
input.case_sensitive,
false,
false,
include_matcher,
PathMatcher::default(), // For now, keep it simple and don't enable an exclude pattern.
true, // Always match file include pattern against *full project paths* that start with a project root.
None,
) {
Ok(query) => query,
Err(error) => return Task::ready(Err(error)).into(),
};
let results = project.update(cx, |project, cx| project.search(query, cx));
cx.spawn(async move|cx| {
futures::pin_mut!(results);
let mut output = String::new();
let mut skips_remaining = input.offset;
let mut matches_found = 0;
let mut has_more_matches = false;
while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
if ranges.is_empty() {
continue;
}
buffer.read_with(cx, |buffer, cx| -> Result<(), anyhow::Error> {
if let Some(path) = buffer.file().map(|file| file.full_path(cx)) {
let mut file_header_written = false;
let mut ranges = ranges
.into_iter()
.map(|range| {
let mut point_range = range.to_point(buffer);
point_range.start.row =
point_range.start.row.saturating_sub(CONTEXT_LINES);
point_range.start.column = 0;
point_range.end.row = cmp::min(
buffer.max_point().row,
point_range.end.row + CONTEXT_LINES,
);
point_range.end.column = buffer.line_len(point_range.end.row);
point_range
})
.peekable();
while let Some(mut range) = ranges.next() {
if skips_remaining > 0 {
skips_remaining -= 1;
continue;
}
// We'd already found a full page of matches, and we just found one more.
if matches_found >= RESULTS_PER_PAGE {
has_more_matches = true;
return Ok(());
}
while let Some(next_range) = ranges.peek() {
if range.end.row >= next_range.start.row {
range.end = next_range.end;
ranges.next();
} else {
break;
}
}
if !file_header_written {
writeln!(output, "\n## Matches in {}", path.display())?;
file_header_written = true;
}
let start_line = range.start.row + 1;
let end_line = range.end.row + 1;
writeln!(output, "\n### Lines {start_line}-{end_line}\n```")?;
output.extend(buffer.text_for_range(range));
output.push_str("\n```\n");
matches_found += 1;
}
}
Ok(())
})??;
}
if matches_found == 0 {
Ok("No matches found".to_string())
} else if has_more_matches {
Ok(format!(
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
input.offset + 1,
input.offset + matches_found,
input.offset + RESULTS_PER_PAGE,
))
} else {
Ok(format!("Found {matches_found} matches:\n{output}"))
}
}).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use assistant_tool::Tool;
use gpui::{AppContext, TestAppContext};
use project::{FakeFs, Project};
use settings::SettingsStore;
use util::path;
#[gpui::test]
async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root",
serde_json::json!({
"src": {
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}",
"utils": {
"helper.rs": "fn helper() {\n println!(\"I'm a helper!\");\n}",
},
},
"tests": {
"test_main.rs": "fn test_main() {\n assert!(true);\n}",
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
// Test with include pattern for Rust files inside the root of the project
let input = serde_json::to_value(GrepToolInput {
regex: "println".to_string(),
include_pattern: Some("root/**/*.rs".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(result.contains("main.rs"), "Should find matches in main.rs");
assert!(
result.contains("helper.rs"),
"Should find matches in helper.rs"
);
assert!(
!result.contains("test_main.rs"),
"Should not include test_main.rs even though it's a .rs file (because it doesn't have the pattern)"
);
// Test with include pattern for src directory only
let input = serde_json::to_value(GrepToolInput {
regex: "fn".to_string(),
include_pattern: Some("root/**/src/**".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
result.contains("main.rs"),
"Should find matches in src/main.rs"
);
assert!(
result.contains("helper.rs"),
"Should find matches in src/utils/helper.rs"
);
assert!(
!result.contains("test_main.rs"),
"Should not include test_main.rs as it's not in src directory"
);
// Test with empty include pattern (should default to all files)
let input = serde_json::to_value(GrepToolInput {
regex: "fn".to_string(),
include_pattern: None,
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(result.contains("main.rs"), "Should find matches in main.rs");
assert!(
result.contains("helper.rs"),
"Should find matches in helper.rs"
);
assert!(
result.contains("test_main.rs"),
"Should include test_main.rs"
);
}
#[gpui::test]
async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root",
serde_json::json!({
"case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true",
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
// Test case-insensitive search (default)
let input = serde_json::to_value(GrepToolInput {
regex: "uppercase".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: false,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
result.contains("UPPERCASE"),
"Case-insensitive search should match uppercase"
);
// Test case-sensitive search
let input = serde_json::to_value(GrepToolInput {
regex: "uppercase".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: true,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
!result.contains("UPPERCASE"),
"Case-sensitive search should not match uppercase"
);
// Test case-sensitive search
let input = serde_json::to_value(GrepToolInput {
regex: "LOWERCASE".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: true,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
!result.contains("lowercase"),
"Case-sensitive search should match lowercase"
);
// Test case-sensitive search for lowercase pattern
let input = serde_json::to_value(GrepToolInput {
regex: "lowercase".to_string(),
include_pattern: Some("**/*.txt".to_string()),
offset: 0,
case_sensitive: true,
})
.unwrap();
let result = run_grep_tool(input, project.clone(), cx).await;
assert!(
result.contains("lowercase"),
"Case-sensitive search should match lowercase text"
);
}
async fn run_grep_tool(
input: serde_json::Value,
project: Entity<Project>,
cx: &mut TestAppContext,
) -> String {
let tool = Arc::new(GrepTool);
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
let task = cx.update(|cx| tool.run(input, &[], project, action_log, cx));
match task.output.await {
Ok(result) => result,
Err(e) => panic!("Failed to run grep tool: {}", e),
}
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
}

View File

@@ -0,0 +1,8 @@
Searches the contents of files in the project with a regular expression
- Prefer this tool to path search when searching for symbols in the project, because you won't need to guess what path it's in.
- Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.)
- Pass an `include_pattern` if you know how to narrow your search on the files system
- Never use this tool to search for paths. Only search file contents with this tool.
- Use this tool when you need to find files containing specific patterns
- Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.

View File

@@ -12,7 +12,7 @@ use util::markdown::MarkdownString;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListDirectoryToolInput {
/// The relative path of the directory to list.
/// The fully-qualified path of the directory to list in the project.
///
/// This path should never be absolute, and the first component
/// of the path should always be a root directory in a project.

View File

@@ -1 +1 @@
Lists files and directories in a given path.
Lists files and directories in a given path. Prefer the `grep` or `path_search` tools when searching the codebase.

View File

@@ -6,14 +6,14 @@ use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat}
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use std::{cmp, fmt::Write as _, path::PathBuf, sync::Arc};
use ui::IconName;
use util::paths::PathMatcher;
use worktree::Snapshot;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct PathSearchToolInput {
/// The glob to search all project paths for.
/// The glob to match against every path in the project.
///
/// <example>
/// If the project has the following root directories:
@@ -76,66 +76,125 @@ impl Tool for PathSearchTool {
Ok(input) => (input.offset, input.glob),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let path_matcher = match PathMatcher::new([
// Sometimes models try to search for "". In this case, return all paths in the project.
if glob.is_empty() { "*" } else { &glob },
]) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))).into(),
};
let snapshots: Vec<Snapshot> = project
.read(cx)
.worktrees(cx)
.map(|worktree| worktree.read(cx).snapshot())
.collect();
let offset = offset as usize;
let task = search_paths(&glob, project, cx);
cx.background_spawn(async move {
let mut matches = Vec::new();
for worktree in snapshots {
let root_name = worktree.root_name();
// Don't consider ignored entries.
for entry in worktree.entries(false, 0) {
if path_matcher.is_match(&entry.path) {
matches.push(
PathBuf::from(root_name)
.join(&entry.path)
.to_string_lossy()
.to_string(),
);
}
}
}
let matches = task.await?;
let paginated_matches = &matches[cmp::min(offset, matches.len())
..cmp::min(offset + RESULTS_PER_PAGE, matches.len())];
if matches.is_empty() {
Ok(format!("No paths in the project matched the glob {glob:?}"))
Ok("No matches found".to_string())
} else {
// Sort to group entries in the same directory together.
matches.sort();
let total_matches = matches.len();
let response = if total_matches > RESULTS_PER_PAGE + offset as usize {
let paginated_matches: Vec<_> = matches
.into_iter()
.skip(offset as usize)
.take(RESULTS_PER_PAGE)
.collect();
format!(
"Found {} total matches. Showing results {}-{} (provide 'offset' parameter for more results):\n\n{}",
total_matches,
let mut message = format!("Found {} total matches.", matches.len());
if matches.len() > RESULTS_PER_PAGE {
write!(
&mut message,
"\nShowing results {}-{} (provide 'offset' parameter for more results):",
offset + 1,
offset as usize + paginated_matches.len(),
paginated_matches.join("\n")
offset + paginated_matches.len()
)
} else {
matches.join("\n")
};
Ok(response)
.unwrap();
}
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
write!(&mut message, "\n{}", mat.display()).unwrap();
}
Ok(message)
}
}).into()
})
.into()
}
}
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
let path_matcher = match PathMatcher::new([
// Sometimes models try to search for "". In this case, return all paths in the project.
if glob.is_empty() { "*" } else { glob },
]) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
};
let snapshots: Vec<Snapshot> = project
.read(cx)
.worktrees(cx)
.map(|worktree| worktree.read(cx).snapshot())
.collect();
cx.background_spawn(async move {
Ok(snapshots
.iter()
.flat_map(|snapshot| {
let root_name = PathBuf::from(snapshot.root_name());
snapshot
.entries(false, 0)
.map(move |entry| root_name.join(&entry.path))
.filter(|path| path_matcher.is_match(&path))
})
.collect())
})
}
#[cfg(test)]
mod test {
use super::*;
use gpui::TestAppContext;
use project::{FakeFs, Project};
use settings::SettingsStore;
use util::path;
#[gpui::test]
async fn test_path_search_tool(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
serde_json::json!({
"apple": {
"banana": {
"carrot": "1",
},
"bandana": {
"carbonara": "2",
},
"endive": "3"
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let matches = cx
.update(|cx| search_paths("root/**/car*", project.clone(), cx))
.await
.unwrap();
assert_eq!(
matches,
&[
PathBuf::from("root/apple/banana/carrot"),
PathBuf::from("root/apple/bandana/carbonara")
]
);
let matches = cx
.update(|cx| search_paths("**/car*", project.clone(), cx))
.await
.unwrap();
assert_eq!(
matches,
&[
PathBuf::from("root/apple/banana/carrot"),
PathBuf::from("root/apple/bandana/carbonara")
]
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
}

View File

@@ -1,3 +1,7 @@
Returns paths in the project which match the given glob.
Fast file pattern matching tool that works with any codebase size
Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages.
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
- Returns matching file paths sorted alphabetically
- Prefer the `grep` tool to this tool when searching for symbols unless you have specific information about paths.
- Use this tool when you need to find files by name patterns
- Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages.

View File

@@ -1,14 +1,14 @@
use std::sync::Arc;
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use indoc::formatdoc;
use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownString;
@@ -95,11 +95,24 @@ impl Tool for ReadFileTool {
};
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path,))).into();
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into();
};
let Some(worktree) = project
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("Worktree not found for project path"))).into();
};
let exists = worktree.update(cx, |worktree, cx| {
worktree.file_exists(&project_path.path, cx)
});
let file_path = input.path.clone();
cx.spawn(async move |cx| {
if !exists.await? {
return Err(anyhow!("{} not found", file_path))
}
let buffer = cx
.update(|cx| {
project.update(cx, |project, cx| project.open_buffer(project_path, cx))
@@ -141,11 +154,231 @@ impl Tool for ReadFileTool {
} else {
// File is too big, so return an error with the outline
// and a suggestion to read again with line numbers.
let outline = file_outline(project, file_path, action_log, None, 0, cx).await?;
let outline = file_outline(project, file_path, action_log, None, cx).await?;
Ok(formatdoc! {"
This file was too big to read all at once. Here is an outline of its symbols:
Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing 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."))
{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."
})
}
}
}).into()
}
}
#[cfg(test)]
mod test {
use super::*;
use gpui::{AppContext, TestAppContext};
use language::{Language, LanguageConfig, LanguageMatcher};
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
use util::path;
#[gpui::test]
async fn test_read_nonexistent_file(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let result = cx
.update(|cx| {
let input = json!({
"path": "root/nonexistent_file.txt"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.output
})
.await;
assert_eq!(
result.unwrap_err().to_string(),
"root/nonexistent_file.txt not found"
);
}
#[gpui::test]
async fn test_read_small_file(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"small_file.txt": "This is a small file content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let result = cx
.update(|cx| {
let input = json!({
"path": "root/small_file.txt"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.output
})
.await;
assert_eq!(result.unwrap(), "This is a small file content");
}
#[gpui::test]
async fn test_read_large_file(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"large_file.rs": (0..1000).map(|i| format!("struct Test{} {{\n a: u32,\n b: usize,\n}}", i)).collect::<Vec<_>>().join("\n")
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(rust_lang()));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let result = cx
.update(|cx| {
let input = json!({
"path": "root/large_file.rs"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log.clone(), cx)
.output
})
.await;
let content = result.unwrap();
assert_eq!(
content.lines().skip(2).take(6).collect::<Vec<_>>(),
vec![
"struct Test0 [L1-4]",
" a [L2]",
" b [L3]",
"struct Test1 [L5-8]",
" a [L6]",
" b [L7]",
]
);
let result = cx
.update(|cx| {
let input = json!({
"path": "root/large_file.rs",
"offset": 1
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.output
})
.await;
let content = result.unwrap();
let expected_content = (0..1000)
.flat_map(|i| {
vec![
format!("struct Test{} [L{}-{}]", i, i * 4 + 1, i * 4 + 4),
format!(" a [L{}]", i * 4 + 2),
format!(" b [L{}]", i * 4 + 3),
]
})
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(
content
.lines()
.skip(2)
.take(expected_content.len())
.collect::<Vec<_>>(),
expected_content
);
}
#[gpui::test]
async fn test_read_file_with_line_range(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let result = cx
.update(|cx| {
let input = json!({
"path": "root/multiline.txt",
"start_line": 2,
"end_line": 4
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.output
})
.await;
assert_eq!(result.unwrap(), "Line 2\nLine 3");
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
fn rust_lang() -> Language {
Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_outline_query(
r#"
(line_comment) @annotation
(struct_item
"struct" @context
name: (_) @name) @item
(enum_item
"enum" @context
name: (_) @name) @item
(enum_variant
name: (_) @name) @item
(field_declaration
name: (_) @name) @item
(impl_item
"impl" @context
trait: (_)? @name
"for"? @context
type: (_) @name
body: (_ "{" (_)* "}")) @item
(function_item
"fn" @context
name: (_) @name) @item
(mod_item
"mod" @context
name: (_) @name) @item
"#,
)
.unwrap()
}
}

View File

@@ -1,6 +1,3 @@
Reads the content of the given file in the project.
If the file is too big to read all at once, and neither a start line
nor an end line was specified, then this returns an outline of the
file's symbols (with line numbers) instead of the file's contents,
so that it can be called again with line ranges.
- Never attempt to read a path that hasn't been previously mentioned.

View File

@@ -1,206 +0,0 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::StreamExt;
use gpui::{App, Entity, Task};
use language::OffsetRangeExt;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{
Project,
search::{SearchQuery, SearchResult},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, fmt::Write, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownString;
use util::paths::PathMatcher;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct RegexSearchToolInput {
/// A regex pattern to search for in the entire project. Note that the regex
/// will be parsed by the Rust `regex` crate.
pub regex: String,
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
pub offset: u32,
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
#[serde(default)]
pub case_sensitive: bool,
}
impl RegexSearchToolInput {
/// Which page of search results this is.
pub fn page(&self) -> u32 {
1 + (self.offset / RESULTS_PER_PAGE)
}
}
const RESULTS_PER_PAGE: u32 = 20;
pub struct RegexSearchTool;
impl Tool for RegexSearchTool {
fn name(&self) -> String {
"regex_search".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./regex_search_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::Regex
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<RegexSearchToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<RegexSearchToolInput>(input.clone()) {
Ok(input) => {
let page = input.page();
let regex_str = MarkdownString::inline_code(&input.regex);
let case_info = if input.case_sensitive {
" (case-sensitive)"
} else {
""
};
if page > 1 {
format!("Get page {page} of search results for regex {regex_str}{case_info}")
} else {
format!("Search files for regex {regex_str}{case_info}")
}
}
Err(_) => "Search with regex".to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
const CONTEXT_LINES: u32 = 2;
let (offset, regex, case_sensitive) =
match serde_json::from_value::<RegexSearchToolInput>(input) {
Ok(input) => (input.offset, input.regex, input.case_sensitive),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let query = match SearchQuery::regex(
&regex,
false,
case_sensitive,
false,
false,
PathMatcher::default(),
PathMatcher::default(),
None,
) {
Ok(query) => query,
Err(error) => return Task::ready(Err(error)).into(),
};
let results = project.update(cx, |project, cx| project.search(query, cx));
cx.spawn(async move|cx| {
futures::pin_mut!(results);
let mut output = String::new();
let mut skips_remaining = offset;
let mut matches_found = 0;
let mut has_more_matches = false;
while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
if ranges.is_empty() {
continue;
}
buffer.read_with(cx, |buffer, cx| -> Result<(), anyhow::Error> {
if let Some(path) = buffer.file().map(|file| file.full_path(cx)) {
let mut file_header_written = false;
let mut ranges = ranges
.into_iter()
.map(|range| {
let mut point_range = range.to_point(buffer);
point_range.start.row =
point_range.start.row.saturating_sub(CONTEXT_LINES);
point_range.start.column = 0;
point_range.end.row = cmp::min(
buffer.max_point().row,
point_range.end.row + CONTEXT_LINES,
);
point_range.end.column = buffer.line_len(point_range.end.row);
point_range
})
.peekable();
while let Some(mut range) = ranges.next() {
if skips_remaining > 0 {
skips_remaining -= 1;
continue;
}
// We'd already found a full page of matches, and we just found one more.
if matches_found >= RESULTS_PER_PAGE {
has_more_matches = true;
return Ok(());
}
while let Some(next_range) = ranges.peek() {
if range.end.row >= next_range.start.row {
range.end = next_range.end;
ranges.next();
} else {
break;
}
}
if !file_header_written {
writeln!(output, "\n## Matches in {}", path.display())?;
file_header_written = true;
}
let start_line = range.start.row + 1;
let end_line = range.end.row + 1;
writeln!(output, "\n### Lines {start_line}-{end_line}\n```")?;
output.extend(buffer.text_for_range(range));
output.push_str("\n```\n");
matches_found += 1;
}
}
Ok(())
})??;
}
if matches_found == 0 {
Ok("No matches found".to_string())
} else if has_more_matches {
Ok(format!(
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
offset + 1,
offset + matches_found,
offset + RESULTS_PER_PAGE,
))
} else {
Ok(format!("Found {matches_found} matches:\n{output}"))
}
}).into()
}
}

View File

@@ -1,7 +0,0 @@
Searches the entire project for the given regular expression.
Returns a list of paths that matched the query. For each path, it returns some excerpts of the matched text.
Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.
This tool is not aware of semantics and does not use any information from language servers, so it should only be used when no available semantic tool (e.g. one that uses language servers) could fit a particular use case instead.

View File

@@ -14,6 +14,7 @@ pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> O
true,
PathMatcher::new(iter::empty::<&str>()).ok()?,
PathMatcher::new(iter::empty::<&str>()).ok()?,
false,
None,
)
.log_err()?;

View File

@@ -0,0 +1,322 @@
use std::{sync::Arc, time::Duration};
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt, TryFutureExt};
use gpui::{
Animation, AnimationExt, App, AppContext, Context, Entity, IntoElement, Task, Window,
pulsating_between,
};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::{IconName, Tooltip, prelude::*};
use web_search::WebSearchRegistry;
use zed_llm_client::{WebSearchCitation, WebSearchResponse};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WebSearchToolInput {
/// The search term or question to query on the web.
query: String,
}
#[derive(RegisterComponent)]
pub struct WebSearchTool;
impl Tool for WebSearchTool {
fn name(&self) -> String {
"web_search".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
"Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into()
}
fn icon(&self) -> IconName {
IconName::Globe
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<WebSearchToolInput>(format)
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
"Web Search".to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<WebSearchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else {
return Task::ready(Err(anyhow!("Web search is not available."))).into();
};
let search_task = provider.search(input.query, cx).map_err(Arc::new).shared();
let output = cx.background_spawn({
let search_task = search_task.clone();
async move {
let response = search_task.await.map_err(|err| anyhow!(err))?;
serde_json::to_string(&response).context("Failed to serialize search results")
}
});
ToolResult {
output,
card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
}
}
}
struct WebSearchToolCard {
response: Option<Result<WebSearchResponse>>,
_task: Task<()>,
}
impl WebSearchToolCard {
fn new(
search_task: impl 'static + Future<Output = Result<WebSearchResponse, Arc<anyhow::Error>>>,
cx: &mut Context<Self>,
) -> Self {
let _task = cx.spawn(async move |this, cx| {
let response = search_task.await.map_err(|err| anyhow!(err));
this.update(cx, |this, cx| {
this.response = Some(response);
cx.notify();
})
.ok();
});
Self {
response: None,
_task,
}
}
}
impl ToolCard for WebSearchToolCard {
fn render(
&mut self,
_status: &ToolUseStatus,
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let header = h_flex()
.id("tool-label-container")
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(
Icon::new(IconName::Globe)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(match self.response.as_ref() {
Some(Ok(response)) => {
let text: SharedString = if response.citations.len() == 1 {
"1 result".into()
} else {
format!("{} results", response.citations.len()).into()
};
h_flex()
.gap_1p5()
.child(Label::new("Searched the Web").size(LabelSize::Small))
.child(
div()
.size(px(3.))
.rounded_full()
.bg(cx.theme().colors().text),
)
.child(Label::new(text).size(LabelSize::Small))
.into_any_element()
}
Some(Err(error)) => div()
.id("web-search-error")
.child(Label::new("Web Search failed").size(LabelSize::Small))
.tooltip(Tooltip::text(error.to_string()))
.into_any_element(),
None => Label::new("Searching the Web…")
.size(LabelSize::Small)
.with_animation(
"web-search-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
)
.into_any_element(),
})
.into_any();
let content =
self.response.as_ref().and_then(|response| match response {
Ok(response) => {
Some(
v_flex()
.ml_1p5()
.pl_1p5()
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.gap_1()
.children(response.citations.iter().enumerate().map(
|(index, citation)| {
let title = citation.title.clone();
let url = citation.url.clone();
Button::new(("citation", index), title)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.truncate(true)
.tooltip({
let url = url.clone();
move |window, cx| {
Tooltip::with_meta(
"Citation Link",
None,
url.clone(),
window,
cx,
)
}
})
.on_click({
let url = url.clone();
move |_, _, cx| cx.open_url(&url)
})
},
))
.into_any(),
)
}
Err(_) => None,
});
v_flex().my_2().gap_1().child(header).children(content)
}
}
impl Component for WebSearchTool {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn sort_name() -> &'static str {
"ToolWebSearch"
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let in_progress_search = cx.new(|cx| WebSearchToolCard {
response: None,
_task: cx.spawn(async move |_this, cx| {
loop {
cx.background_executor()
.timer(Duration::from_secs(60))
.await
}
}),
});
let successful_search = cx.new(|_cx| WebSearchToolCard {
response: Some(Ok(example_search_response())),
_task: Task::ready(()),
});
let error_search = cx.new(|_cx| WebSearchToolCard {
response: Some(Err(anyhow!("Failed to resolve https://google.com"))),
_task: Task::ready(()),
});
Some(
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example(
"In Progress",
div()
.size_full()
.child(in_progress_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Pending, window, cx)
.into_any_element()
}))
.into_any_element(),
),
single_example(
"Successful",
div()
.size_full()
.child(successful_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Finished("".into()), window, cx)
.into_any_element()
}))
.into_any_element(),
),
single_example(
"Error",
div()
.size_full()
.child(error_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Error("".into()), window, cx)
.into_any_element()
}))
.into_any_element(),
),
])])
.into_any_element(),
)
}
}
fn example_search_response() -> WebSearchResponse {
WebSearchResponse {
summary: r#"Toronto boasts a vibrant culinary scene with a diverse array of..."#
.to_string(),
citations: vec![
WebSearchCitation {
title: "Alo".to_string(),
url: "https://www.google.com/maps/search/Alo%2C+Toronto%2C+Canada".to_string(),
range: Some(147..213),
},
WebSearchCitation {
title: "Edulis".to_string(),
url: "https://www.google.com/maps/search/Edulis%2C+Toronto%2C+Canada".to_string(),
range: Some(447..519),
},
WebSearchCitation {
title: "Sushi Masaki Saito".to_string(),
url: "https://www.google.com/maps/search/Sushi+Masaki+Saito%2C+Toronto%2C+Canada"
.to_string(),
range: Some(776..872),
},
WebSearchCitation {
title: "Shoushin".to_string(),
url: "https://www.google.com/maps/search/Shoushin%2C+Toronto%2C+Canada".to_string(),
range: Some(1072..1148),
},
WebSearchCitation {
title: "Restaurant 20 Victoria".to_string(),
url:
"https://www.google.com/maps/search/Restaurant+20+Victoria%2C+Toronto%2C+Canada"
.to_string(),
range: Some(1291..1395),
},
],
}
}

View File

@@ -84,6 +84,10 @@ pub enum Model {
}
impl Model {
pub fn default_fast() -> Self {
Self::Claude3_5Haiku
}
pub fn from_id(id: &str) -> anyhow::Result<Self> {
if id.starts_with("claude-3-5-sonnet-v2") {
Ok(Self::Claude3_5SonnetV2)

View File

@@ -75,6 +75,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
assistant = { workspace = true, features = ["test-support"] }

View File

@@ -117,6 +117,7 @@ CREATE TABLE "project_repositories" (
"is_deleted" BOOL NOT NULL,
"current_merge_conflicts" VARCHAR,
"branch_summary" VARCHAR,
"head_commit_details" VARCHAR,
PRIMARY KEY (project_id, id)
);

View File

@@ -0,0 +1,10 @@
create table subscription_usages (
id serial primary key,
user_id integer not null,
period_start_at timestamp without time zone not null,
period_end_at timestamp without time zone not null,
model_requests int not null default 0,
edit_predictions int not null default 0
);
create unique index uix_subscription_usages_on_user_id_start_at_end_at on subscription_usages (user_id, period_start_at, period_end_at);

View File

@@ -0,0 +1,4 @@
alter table subscription_usages
add column plan text not null;
create index ix_subscription_usages_on_plan on subscription_usages (plan);

View File

@@ -15,10 +15,12 @@ use stripe::{
BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession,
CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion,
CreateBillingPortalSessionFlowDataAfterCompletionRedirect,
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm,
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems,
CreateBillingPortalSessionFlowDataType, CreateCustomer, Customer, CustomerId, EventObject,
EventType, Expandable, ListEvents, Subscription, SubscriptionId, SubscriptionStatus,
};
use util::ResultExt;
use util::{ResultExt, maybe};
use crate::api::events::SnowflakeRow;
use crate::db::billing_subscription::{
@@ -52,6 +54,7 @@ pub fn router() -> Router {
post(manage_billing_subscription),
)
.route("/billing/monthly_spend", get(get_monthly_spend))
.route("/billing/usage", get(get_current_usage))
}
#[derive(Debug, Deserialize)]
@@ -159,6 +162,7 @@ struct BillingSubscriptionJson {
id: BillingSubscriptionId,
name: String,
status: StripeSubscriptionStatus,
trial_end_at: Option<String>,
cancel_at: Option<String>,
/// Whether this subscription can be canceled.
is_cancelable: bool,
@@ -188,9 +192,21 @@ async fn list_billing_subscriptions(
id: subscription.id,
name: match subscription.kind {
Some(SubscriptionKind::ZedPro) => "Zed Pro".to_string(),
Some(SubscriptionKind::ZedProTrial) => "Zed Pro (Trial)".to_string(),
Some(SubscriptionKind::ZedFree) => "Zed Free".to_string(),
None => "Zed LLM Usage".to_string(),
},
status: subscription.stripe_subscription_status,
trial_end_at: if subscription.kind == Some(SubscriptionKind::ZedProTrial) {
maybe!({
let end_at = subscription.stripe_current_period_end?;
let end_at = DateTime::from_timestamp(end_at, 0)?;
Some(end_at.to_rfc3339_opts(SecondsFormat::Millis, true))
})
} else {
None
},
cancel_at: subscription.stripe_cancel_at.map(|cancel_at| {
cancel_at
.and_utc()
@@ -207,6 +223,7 @@ async fn list_billing_subscriptions(
#[serde(rename_all = "snake_case")]
enum ProductCode {
ZedPro,
ZedProTrial,
}
#[derive(Debug, Deserialize)]
@@ -286,24 +303,38 @@ async fn create_billing_subscription(
customer.id
};
let success_url = format!(
"{}/account?checkout_complete=1",
app.config.zed_dot_dev_url()
);
let checkout_session_url = match body.product {
Some(ProductCode::ZedPro) => {
let success_url = format!(
"{}/account?checkout_complete=1",
app.config.zed_dot_dev_url()
);
stripe_billing
.checkout_with_zed_pro(customer_id, &user.github_login, &success_url)
.checkout_with_price(
app.config.zed_pro_price_id()?,
customer_id,
&user.github_login,
&success_url,
)
.await?
}
Some(ProductCode::ZedProTrial) => {
stripe_billing
.checkout_with_zed_pro_trial(
app.config.zed_pro_price_id()?,
customer_id,
&user.github_login,
&success_url,
)
.await?
}
None => {
let default_model =
llm_db.model(rpc::LanguageModelProvider::Anthropic, "claude-3-7-sonnet")?;
let default_model = llm_db.model(
zed_llm_client::LanguageModelProvider::Anthropic,
"claude-3-7-sonnet",
)?;
let stripe_model = stripe_billing.register_model(default_model).await?;
let success_url = format!(
"{}/account?checkout_complete=1",
app.config.zed_dot_dev_url()
);
stripe_billing
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
.await?
@@ -322,6 +353,8 @@ enum ManageSubscriptionIntent {
///
/// This will open the Stripe billing portal without putting the user in a specific flow.
ManageSubscription,
/// The user intends to upgrade to Zed Pro.
UpgradeToPro,
/// The user intends to cancel their subscription.
Cancel,
/// The user intends to stop the cancellation of their subscription.
@@ -373,11 +406,10 @@ async fn manage_billing_subscription(
.get_billing_subscription_by_id(body.subscription_id)
.await?
.ok_or_else(|| anyhow!("subscription not found"))?;
let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
.context("failed to parse subscription ID")?;
if body.intent == ManageSubscriptionIntent::StopCancellation {
let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id)
.context("failed to parse subscription ID")?;
let updated_stripe_subscription = Subscription::update(
&stripe_client,
&subscription_id,
@@ -410,6 +442,69 @@ async fn manage_billing_subscription(
let flow = match body.intent {
ManageSubscriptionIntent::ManageSubscription => None,
ManageSubscriptionIntent::UpgradeToPro => {
let zed_pro_price_id = app.config.zed_pro_price_id()?;
let zed_free_price_id = app.config.zed_free_price_id()?;
let stripe_subscription =
Subscription::retrieve(&stripe_client, &subscription_id, &[]).await?;
let is_on_zed_pro_trial = stripe_subscription.status == SubscriptionStatus::Trialing
&& stripe_subscription.items.data.iter().any(|item| {
item.price
.as_ref()
.map_or(false, |price| price.id == zed_pro_price_id)
});
if is_on_zed_pro_trial {
// If the user is already on a Zed Pro trial and wants to upgrade to Pro, we just need to end their trial early.
Subscription::update(
&stripe_client,
&stripe_subscription.id,
stripe::UpdateSubscription {
trial_end: Some(stripe::Scheduled::now()),
..Default::default()
},
)
.await?;
return Ok(Json(ManageBillingSubscriptionResponse {
billing_portal_session_url: None,
}));
}
let subscription_item_to_update = stripe_subscription
.items
.data
.iter()
.find_map(|item| {
let price = item.price.as_ref()?;
if price.id == zed_free_price_id {
Some(item.id.clone())
} else {
None
}
})
.ok_or_else(|| anyhow!("No subscription item to update"))?;
Some(CreateBillingPortalSessionFlowData {
type_: CreateBillingPortalSessionFlowDataType::SubscriptionUpdateConfirm,
subscription_update_confirm: Some(
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm {
subscription: subscription.stripe_subscription_id,
items: vec![
CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems {
id: subscription_item_to_update.to_string(),
price: Some(zed_pro_price_id.to_string()),
quantity: Some(1),
},
],
discounts: None,
},
),
..Default::default()
})
}
ManageSubscriptionIntent::Cancel => Some(CreateBillingPortalSessionFlowData {
type_: CreateBillingPortalSessionFlowDataType::SubscriptionCancel,
after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion {
@@ -696,22 +791,26 @@ async fn handle_customer_subscription_event(
log::info!("handling Stripe {} event: {}", event.type_, event.id);
let subscription_kind =
if let Some(zed_pro_price_id) = app.config.stripe_zed_pro_price_id.as_deref() {
let has_zed_pro_price = subscription.items.data.iter().any(|item| {
item.price
.as_ref()
.map_or(false, |price| price.id.as_str() == zed_pro_price_id)
});
let subscription_kind = maybe!({
let zed_pro_price_id = app.config.zed_pro_price_id().ok()?;
let zed_free_price_id = app.config.zed_free_price_id().ok()?;
if has_zed_pro_price {
Some(SubscriptionKind::ZedPro)
subscription.items.data.iter().find_map(|item| {
let price = item.price.as_ref()?;
if price.id == zed_pro_price_id {
Some(if subscription.status == SubscriptionStatus::Trialing {
SubscriptionKind::ZedProTrial
} else {
SubscriptionKind::ZedPro
})
} else if price.id == zed_free_price_id {
Some(SubscriptionKind::ZedFree)
} else {
None
}
} else {
None
};
})
});
let billing_customer =
find_or_create_billing_customer(app, stripe_client, subscription.customer)
@@ -874,6 +973,105 @@ async fn get_monthly_spend(
}))
}
#[derive(Debug, Deserialize)]
struct GetCurrentUsageParams {
github_user_id: i32,
}
#[derive(Debug, Serialize)]
struct UsageCounts {
pub used: i32,
pub limit: Option<i32>,
pub remaining: Option<i32>,
}
#[derive(Debug, Serialize)]
struct GetCurrentUsageResponse {
pub model_requests: UsageCounts,
pub edit_predictions: UsageCounts,
}
async fn get_current_usage(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<GetCurrentUsageParams>,
) -> Result<Json<GetCurrentUsageResponse>> {
let user = app
.db
.get_user_by_github_user_id(params.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
let Some(llm_db) = app.llm_db.clone() else {
return Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"LLM database not available".into(),
));
};
let empty_usage = GetCurrentUsageResponse {
model_requests: UsageCounts {
used: 0,
limit: Some(0),
remaining: Some(0),
},
edit_predictions: UsageCounts {
used: 0,
limit: Some(0),
remaining: Some(0),
},
};
let Some(subscription) = app.db.get_active_billing_subscription(user.id).await? else {
return Ok(Json(empty_usage));
};
let subscription_period = maybe!({
let period_start_at = subscription.current_period_start_at()?;
let period_end_at = subscription.current_period_end_at()?;
Some((period_start_at, period_end_at))
});
let Some((period_start_at, period_end_at)) = subscription_period else {
return Ok(Json(empty_usage));
};
let usage = llm_db
.get_subscription_usage_for_period(user.id, period_start_at, period_end_at)
.await?;
let Some(usage) = usage else {
return Ok(Json(empty_usage));
};
let plan = match usage.plan {
SubscriptionKind::ZedPro => zed_llm_client::Plan::ZedPro,
SubscriptionKind::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
SubscriptionKind::ZedFree => zed_llm_client::Plan::Free,
};
let model_requests_limit = match plan.model_requests_limit() {
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
zed_llm_client::UsageLimit::Unlimited => None,
};
let edit_prediction_limit = match plan.edit_predictions_limit() {
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
zed_llm_client::UsageLimit::Unlimited => None,
};
Ok(Json(GetCurrentUsageResponse {
model_requests: UsageCounts {
used: usage.model_requests,
limit: model_requests_limit,
remaining: model_requests_limit.map(|limit| (limit - usage.model_requests).max(0)),
},
edit_predictions: UsageCounts {
used: usage.edit_predictions,
limit: edit_prediction_limit,
remaining: edit_prediction_limit.map(|limit| (limit - usage.edit_predictions).max(0)),
},
}))
}
impl From<SubscriptionStatus> for StripeSubscriptionStatus {
fn from(value: SubscriptionStatus) -> Self {
match value {

View File

@@ -62,11 +62,14 @@ impl Database {
billing_subscription::Entity::update(billing_subscription::ActiveModel {
id: ActiveValue::set(id),
billing_customer_id: params.billing_customer_id.clone(),
kind: params.kind.clone(),
stripe_subscription_id: params.stripe_subscription_id.clone(),
stripe_subscription_status: params.stripe_subscription_status.clone(),
stripe_cancel_at: params.stripe_cancel_at.clone(),
stripe_cancellation_reason: params.stripe_cancellation_reason.clone(),
..Default::default()
stripe_current_period_start: params.stripe_current_period_start.clone(),
stripe_current_period_end: params.stripe_current_period_end.clone(),
created_at: ActiveValue::not_set(),
})
.exec(&*tx)
.await?;
@@ -105,6 +108,35 @@ impl Database {
.await
}
pub async fn get_active_billing_subscription(
&self,
user_id: UserId,
) -> Result<Option<billing_subscription::Model>> {
self.transaction(|tx| async move {
Ok(billing_subscription::Entity::find()
.inner_join(billing_customer::Entity)
.filter(billing_customer::Column::UserId.eq(user_id))
.filter(
Condition::all()
.add(
Condition::any()
.add(
billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::Active),
)
.add(
billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::Trialing),
),
)
.add(billing_subscription::Column::Kind.is_not_null()),
)
.one(&*tx)
.await?)
})
.await
}
/// Returns all of the billing subscriptions for the user with the specified ID.
///
/// Note that this returns the subscriptions regardless of their status.
@@ -142,6 +174,7 @@ impl Database {
billing_subscription::Column::StripeSubscriptionStatus
.eq(StripeSubscriptionStatus::Active),
)
.filter(billing_subscription::Column::Kind.is_null())
.order_by_asc(billing_subscription::Column::Id)
.stream(&*tx)
.await?;

View File

@@ -348,9 +348,10 @@ impl Database {
.unwrap(),
)),
// Old clients do not use abs path or entry ids.
// Old clients do not use abs path, entry ids or head_commit_details.
abs_path: ActiveValue::set(String::new()),
entry_ids: ActiveValue::set("[]".into()),
head_commit_details: ActiveValue::set(None),
}
}),
)
@@ -490,6 +491,12 @@ impl Database {
.as_ref()
.map(|summary| serde_json::to_string(summary).unwrap()),
),
head_commit_details: ActiveValue::Set(
update
.head_commit_details
.as_ref()
.map(|details| serde_json::to_string(details).unwrap()),
),
current_merge_conflicts: ActiveValue::Set(Some(
serde_json::to_string(&update.current_merge_conflicts).unwrap(),
)),
@@ -505,6 +512,7 @@ impl Database {
project_repository::Column::EntryIds,
project_repository::Column::AbsPath,
project_repository::Column::CurrentMergeConflicts,
project_repository::Column::HeadCommitDetails,
])
.to_owned(),
)
@@ -928,6 +936,13 @@ impl Database {
.transpose()?
.unwrap_or_default();
let head_commit_details = db_repository_entry
.head_commit_details
.as_ref()
.map(|head_commit_details| serde_json::from_str(&head_commit_details))
.transpose()?
.unwrap_or_default();
let entry_ids = serde_json::from_str(&db_repository_entry.entry_ids)
.context("failed to deserialize repository's entry ids")?;
@@ -954,6 +969,7 @@ impl Database {
removed_statuses: Vec::new(),
current_merge_conflicts,
branch_summary,
head_commit_details,
scan_id: db_repository_entry.scan_id as u64,
is_last_update: true,
});

View File

@@ -755,6 +755,13 @@ impl Database {
.transpose()?
.unwrap_or_default();
let head_commit_details = db_repository
.head_commit_details
.as_ref()
.map(|head_commit_details| serde_json::from_str(&head_commit_details))
.transpose()?
.unwrap_or_default();
let entry_ids = serde_json::from_str(&db_repository.entry_ids)
.context("failed to deserialize repository's entry ids")?;
@@ -778,6 +785,7 @@ impl Database {
removed_statuses,
current_merge_conflicts,
branch_summary,
head_commit_details,
project_id: project_id.to_proto(),
id: db_repository.id as u64,
abs_path: db_repository.abs_path,

View File

@@ -19,6 +19,18 @@ pub struct Model {
pub created_at: DateTime,
}
impl Model {
pub fn current_period_start_at(&self) -> Option<DateTimeUtc> {
let period_start = self.stripe_current_period_start?;
chrono::DateTime::from_timestamp(period_start, 0)
}
pub fn current_period_end_at(&self) -> Option<DateTimeUtc> {
let period_end = self.stripe_current_period_end?;
chrono::DateTime::from_timestamp(period_end, 0)
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
@@ -43,6 +55,10 @@ impl ActiveModelBehavior for ActiveModel {}
pub enum SubscriptionKind {
#[sea_orm(string_value = "zed_pro")]
ZedPro,
#[sea_orm(string_value = "zed_pro_trial")]
ZedProTrial,
#[sea_orm(string_value = "zed_free")]
ZedFree,
}
/// The status of a Stripe subscription.

View File

@@ -18,6 +18,8 @@ pub struct Model {
pub current_merge_conflicts: Option<String>,
// A JSON object representing the current Branch values
pub branch_summary: Option<String>,
// A JSON object representing the current Head commit values
pub head_commit_details: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -183,6 +183,8 @@ pub struct Config {
pub auto_join_channel_id: Option<ChannelId>,
pub stripe_api_key: Option<String>,
pub stripe_zed_pro_price_id: Option<String>,
pub stripe_zed_pro_trial_price_id: Option<String>,
pub stripe_zed_free_price_id: Option<String>,
pub supermaven_admin_api_key: Option<Arc<str>>,
pub user_backfiller_github_access_token: Option<Arc<str>>,
}
@@ -201,6 +203,22 @@ impl Config {
}
}
pub fn zed_pro_price_id(&self) -> anyhow::Result<stripe::PriceId> {
Self::parse_stripe_price_id("Zed Pro", self.stripe_zed_pro_price_id.as_deref())
}
pub fn zed_free_price_id(&self) -> anyhow::Result<stripe::PriceId> {
Self::parse_stripe_price_id("Zed Free", self.stripe_zed_pro_price_id.as_deref())
}
fn parse_stripe_price_id(name: &str, value: Option<&str>) -> anyhow::Result<stripe::PriceId> {
use std::str::FromStr as _;
let price_id = value.ok_or_else(|| anyhow!("{name} price ID not set"))?;
Ok(stripe::PriceId::from_str(price_id)?)
}
#[cfg(test)]
pub fn test() -> Self {
Self {
@@ -239,6 +257,8 @@ impl Config {
seed_path: None,
stripe_api_key: None,
stripe_zed_pro_price_id: None,
stripe_zed_pro_trial_price_id: None,
stripe_zed_free_price_id: None,
supermaven_admin_api_key: None,
user_backfiller_github_access_token: None,
kinesis_region: None,
@@ -324,12 +344,9 @@ impl AppState {
llm_db,
livekit_client,
blob_store_client: build_blob_store_client(&config).await.log_err(),
stripe_billing: stripe_client.clone().map(|stripe_client| {
Arc::new(StripeBilling::new(
stripe_client,
config.stripe_zed_pro_price_id.clone(),
))
}),
stripe_billing: stripe_client
.clone()
.map(|stripe_client| Arc::new(StripeBilling::new(stripe_client))),
stripe_client,
rate_limiter: Arc::new(RateLimiter::new(db)),
executor,

View File

@@ -8,9 +8,9 @@ mod tests;
use collections::HashMap;
pub use ids::*;
use rpc::LanguageModelProvider;
pub use seed::*;
pub use tables::*;
use zed_llm_client::LanguageModelProvider;
#[cfg(test)]
pub use tests::TestLlmDb;

View File

@@ -2,4 +2,5 @@ use super::*;
pub mod billing_events;
pub mod providers;
pub mod subscription_usages;
pub mod usages;

View File

@@ -0,0 +1,22 @@
use crate::db::UserId;
use super::*;
impl LlmDatabase {
pub async fn get_subscription_usage_for_period(
&self,
user_id: UserId,
period_start_at: DateTimeUtc,
period_end_at: DateTimeUtc,
) -> Result<Option<subscription_usage::Model>> {
self.transaction(|tx| async move {
Ok(subscription_usage::Entity::find()
.filter(subscription_usage::Column::UserId.eq(user_id))
.filter(subscription_usage::Column::PeriodStartAt.eq(period_start_at))
.filter(subscription_usage::Column::PeriodEndAt.eq(period_end_at))
.one(&*tx)
.await?)
})
.await
}
}

View File

@@ -2,5 +2,6 @@ pub mod billing_event;
pub mod model;
pub mod monthly_usage;
pub mod provider;
pub mod subscription_usage;
pub mod usage;
pub mod usage_measure;

View File

@@ -0,0 +1,22 @@
use crate::db::UserId;
use crate::db::billing_subscription::SubscriptionKind;
use sea_orm::entity::prelude::*;
use time::PrimitiveDateTime;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "subscription_usages")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: UserId,
pub period_start_at: PrimitiveDateTime,
pub period_end_at: PrimitiveDateTime,
pub plan: SubscriptionKind,
pub model_requests: i32,
pub edit_predictions: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -1,5 +1,5 @@
use pretty_assertions::assert_eq;
use rpc::LanguageModelProvider;
use zed_llm_client::LanguageModelProvider;
use crate::llm::db::LlmDatabase;
use crate::test_llm_db;

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