Compare commits

..

162 Commits

Author SHA1 Message Date
Thomas Mickley-Doyle
6f5d0cfe00 Call shell for each env 2025-04-21 10:19:34 -05:00
Thomas Mickley-Doyle
a41a4fb145 Run with bash 2025-04-21 08:26:54 -05:00
Thomas Mickley-Doyle
1ef424f1aa Remove running with bash 2025-04-21 08:25:58 -05:00
Thomas Mickley-Doyle
b36b205a6f Run eval with bash 2025-04-21 08:25:17 -05:00
Thomas Mickley-Doyle
83e4cd1553 Run evals in GA 2025-04-18 22:01:36 -05:00
Thomas Mickley-Doyle
65502c0689 Running evals from github actions 2025-04-18 21:47:54 -05:00
Nathan Sobo
61315388cf Replace todo! with TODO: to avoid formatting issue
We want to fix this soon.

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-04-18 20:27:25 -06:00
Nathan Sobo
01f9bff428 Temporarily restore the old attach_tool_results implementation
Thinking models are broken with the reverted change, but I'd still like
to not do it this way.

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-04-18 20:07:39 -06:00
Nathan Sobo
0a9e5e4586 Merge remote-tracking branch 'origin/main' into reboot-agent-loop 2025-04-18 19:45:33 -06:00
Danilo Leal
8102a16747 agent: Make copy button show while hovering the codeblock container (#29075) 2025-04-18 22:14:43 -03:00
Max Brunsfeld
bc7723b5fe Remove extra code fence lines from tool output in eval example files
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-04-18 18:13:35 -07:00
Agus Zubiaga
128b2d4bff Remove abs_path from system prompt
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-04-18 18:12:37 -07: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
Agus Zubiaga
c7b4e5e1ac eval: Run judges concurrently (#29039)
Release Notes:

- N/A
2025-04-18 10:20:43 -03: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
Antonio Scandurra
8aea590646 Avoid telling the model to provide an offset if results fit in a page 2025-04-18 13:49:20 +02:00
Antonio Scandurra
9cf88d21aa Always invoke request callback, even when there's an error
This lets us inspect what went wrong
2025-04-18 13:39:48 +02:00
Antonio Scandurra
50418ab113 Show "no matches found" when path_search_tool doesn't find anything 2025-04-18 13:13:15 +02:00
Antonio Scandurra
4fe6ae52f8 Test read_file and return an error when file does not exist 2025-04-18 12:29:14 +02: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
Max Brunsfeld
e9bb15b906 Normalize line endings in judge prompt tests 2025-04-17 17:41:33 -07:00
Max Brunsfeld
f77e752908 Remove duplicate critera and judge_prompt files 2025-04-17 17:24:51 -07:00
Agus Zubiaga
28b1e08c0c eval: Improve lang server idle detection (#29013)
More accurately detect when the language server is idle in the eval.
This helps ensure that the diagnostics tool will work and that we will
accurately report them to the judge.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2025-04-17 21:16:08 -03:00
Max Brunsfeld
69b6aef396 Don't check typos within any eval examples 2025-04-17 17:14:53 -07:00
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
Max Brunsfeld
6e825281de Remove todo 2025-04-17 16:35:10 -07:00
Nathan Sobo
5d5377d838 Reboot agent loop
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>
2025-04-17 16:32:04 -07: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
301 changed files with 9063 additions and 3951 deletions

1
.clinerules Symbolic link
View File

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

View File

@@ -20,6 +20,7 @@ platforms = [
[traversal-excludes]
workspace-members = [
"remote_server",
"eval",
]
third-party = [
{ name = "reqwest", version = "0.11.27" },

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

@@ -325,7 +325,7 @@ jobs:
cache-provider: "buildjet"
- name: Install Clang & Mold
run: ./script/remote-server && ./script/install-mold 2.34.0
run: ./script/headless && ./script/install-mold 2.34.0
- name: Configure CI
run: |
@@ -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,92 @@
name: Run Eval Daily
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
jobs:
build_binary:
name: Build Eval Binary
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxcb-shape0-dev libxcb-xfixes0-dev libxcb1-dev libxcb-render0-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-keysyms1-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxfixes-dev pkg-config libasound2-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev
./script/headless && ./script/install-mold 2.34.0
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Build entire Zed application
run: cargo build --release
- name: Build eval binary separately to ensure it's available
run: cargo build --release -p eval
- name: Upload eval binary
uses: actions/upload-artifact@v4
with:
name: eval-binary
path: target/release/eval
run_eval:
name: Run Eval - ${{ matrix.exercise }}
needs: build_binary
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
exercise:
- find_and_replace_diff_card
- metal_i64_support
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxcb-shape0-dev libxcb-xfixes0-dev libxcb1-dev libxcb-render0-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-keysyms1-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxfixes-dev pkg-config libasound2-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev
./script/headless && ./script/install-mold 2.34.0
- name: Create required directories
run: |
mkdir -p /home/runner/.config/zed
touch /home/runner/.config/zed/settings.json
mkdir -p ./crates/eval/repos
mkdir -p ./crates/eval/worktrees
mkdir -p ./crates/eval/runs
- name: Set up Anthropic API key
run: echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}" >> $GITHUB_ENV
- name: Download eval binary
uses: actions/download-artifact@v4
with:
name: eval-binary
path: ./target/release/
- name: Make eval binary executable
run: chmod +x ./target/release/eval
- name: Run eval for ${{ matrix.exercise }}
env:
SHELL: /bin/bash
run: ./target/release/eval --cohort-id dailyrun${{ github.run_id }} ${{ matrix.exercise }}

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

1
CLAUDE.md Symbolic link
View File

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

239
Cargo.lock generated
View File

@@ -125,6 +125,7 @@ dependencies = [
"workspace",
"workspace-hack",
"zed_actions",
"zed_llm_client",
]
[[package]]
@@ -324,7 +325,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.27.1",
"thiserror 2.0.12",
"workspace-hack",
]
@@ -337,9 +338,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 +449,6 @@ dependencies = [
"smol",
"tempfile",
"util",
"which 6.0.3",
"workspace-hack",
]
@@ -567,7 +567,7 @@ dependencies = [
"settings",
"smallvec",
"smol",
"strum",
"strum 0.27.1",
"telemetry_events",
"text",
"theme",
@@ -704,14 +704,19 @@ 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",
@@ -719,12 +724,15 @@ dependencies = [
"serde",
"serde_json",
"settings",
"tree-sitter-rust",
"ui",
"unindent",
"util",
"web_search",
"workspace",
"workspace-hack",
"worktree",
"zed_llm_client",
]
[[package]]
@@ -1336,12 +1344,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",
@@ -1353,6 +1362,7 @@ dependencies = [
"bytes 1.10.1",
"fastrand 2.3.0",
"http 0.2.12",
"hyper 0.14.32",
"once_cell",
"regex-lite",
"tracing",
@@ -1882,7 +1892,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.27.1",
"thiserror 2.0.12",
"tokio",
"workspace-hack",
@@ -2716,9 +2726,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",
@@ -2726,9 +2736,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",
@@ -3029,7 +3039,7 @@ dependencies = [
"settings",
"sha2",
"sqlx",
"strum",
"strum 0.27.1",
"subtle",
"supermaven_api",
"telemetry_events",
@@ -3049,6 +3059,7 @@ dependencies = [
"workspace",
"workspace-hack",
"worktree",
"zed_llm_client",
]
[[package]]
@@ -3361,7 +3372,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum",
"strum 0.27.1",
"task",
"theme",
"ui",
@@ -4478,7 +4489,7 @@ dependencies = [
"optfield",
"proc-macro2",
"quote",
"strum",
"strum 0.26.3",
"syn 2.0.100",
]
@@ -4887,6 +4898,7 @@ dependencies = [
"collections",
"context_server",
"dap",
"dirs 5.0.1",
"env_logger 0.11.8",
"extension",
"fs",
@@ -4909,39 +4921,11 @@ dependencies = [
"serde_json",
"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",
]
@@ -5121,7 +5105,7 @@ dependencies = [
"serde",
"settings",
"smallvec",
"strum",
"strum 0.27.1",
"telemetry",
"theme",
"ui",
@@ -5972,7 +5956,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
"strum",
"strum 0.27.1",
"telemetry",
"theme",
"time",
@@ -6065,7 +6049,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.27.1",
"workspace-hack",
]
@@ -6171,7 +6155,7 @@ dependencies = [
"slotmap",
"smallvec",
"smol",
"strum",
"strum 0.27.1",
"sum_tree",
"taffy",
"thiserror 2.0.12",
@@ -6819,7 +6803,7 @@ name = "icons"
version = "0.1.0"
dependencies = [
"serde",
"strum",
"strum 0.27.1",
"workspace-hack",
]
@@ -7087,16 +7071,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",
@@ -7130,10 +7114,12 @@ dependencies = [
name = "inline_completion"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"language",
"project",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -7164,6 +7150,7 @@ dependencies = [
"workspace",
"workspace-hack",
"zed_actions",
"zed_llm_client",
"zeta",
]
@@ -7673,11 +7660,12 @@ dependencies = [
"serde",
"serde_json",
"smol",
"strum",
"strum 0.27.1",
"telemetry_events",
"thiserror 2.0.12",
"util",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -7733,7 +7721,7 @@ dependencies = [
"serde_json",
"settings",
"smol",
"strum",
"strum 0.27.1",
"theme",
"thiserror 2.0.12",
"tiktoken-rs",
@@ -7741,6 +7729,7 @@ dependencies = [
"ui",
"util",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -7902,9 +7891,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"
@@ -8705,7 +8694,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.27.1",
"workspace-hack",
]
@@ -9552,7 +9541,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.27.1",
"workspace-hack",
]
@@ -10801,9 +10790,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",
]
@@ -11746,6 +11735,7 @@ name = "remote_server"
version = "0.1.0"
dependencies = [
"anyhow",
"askpass",
"async-watch",
"backtrace",
"cargo_toml",
@@ -12131,7 +12121,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"strum",
"strum 0.27.1",
"tracing",
"util",
"workspace-hack",
@@ -12640,9 +12630,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",
@@ -12659,7 +12649,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"strum",
"strum 0.26.3",
"thiserror 2.0.12",
"time",
"tracing",
@@ -12669,9 +12659,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",
@@ -13255,9 +13245,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",
]
@@ -13324,6 +13314,7 @@ dependencies = [
"fs",
"futures 0.3.31",
"gpui",
"indoc",
"parking_lot",
"paths",
"schemars",
@@ -13452,9 +13443,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",
@@ -13465,10 +13456,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",
@@ -13489,7 +13481,6 @@ dependencies = [
"percent-encoding",
"rust_decimal",
"rustls 0.23.26",
"rustls-pemfile 2.2.0",
"serde",
"serde_json",
"sha2",
@@ -13506,9 +13497,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",
@@ -13519,9 +13510,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",
@@ -13545,9 +13536,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",
@@ -13592,9 +13583,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",
@@ -13635,9 +13626,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",
@@ -13653,6 +13644,7 @@ dependencies = [
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.12",
"time",
"tracing",
"url",
@@ -13704,7 +13696,7 @@ dependencies = [
"settings",
"simplelog",
"story",
"strum",
"strum 0.27.1",
"theme",
"title_bar",
"ui",
@@ -13786,7 +13778,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]]
@@ -13802,6 +13803,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"
@@ -14417,7 +14431,7 @@ dependencies = [
"serde_json_lenient",
"serde_repr",
"settings",
"strum",
"strum 0.27.1",
"thiserror 2.0.12",
"util",
"uuid",
@@ -14451,7 +14465,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"simplelog",
"strum",
"strum 0.27.1",
"theme",
"vscode_theme",
"workspace-hack",
@@ -15452,7 +15466,7 @@ dependencies = [
"settings",
"smallvec",
"story",
"strum",
"strum 0.27.1",
"theme",
"ui_macros",
"util",
@@ -15826,7 +15840,6 @@ dependencies = [
"indoc",
"itertools 0.14.0",
"language",
"libc",
"log",
"lsp",
"multi_buffer",
@@ -16585,6 +16598,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"
@@ -17623,7 +17666,7 @@ dependencies = [
"settings",
"smallvec",
"sqlez",
"strum",
"strum 0.27.1",
"task",
"telemetry",
"tempfile",
@@ -17768,7 +17811,7 @@ dependencies = [
"sqlx-macros-core",
"sqlx-postgres",
"sqlx-sqlite",
"strum",
"strum 0.26.3",
"subtle",
"syn 1.0.109",
"syn 2.0.100",
@@ -18140,12 +18183,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",
@@ -18263,6 +18307,8 @@ dependencies = [
"uuid",
"vim",
"vim_mode_setting",
"web_search",
"web_search_providers",
"welcome",
"windows 0.61.1",
"winresource",
@@ -18281,6 +18327,7 @@ dependencies = [
"gpui",
"schemars",
"serde",
"uuid",
"workspace-hack",
]
@@ -18327,12 +18374,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

@@ -47,7 +47,6 @@ members = [
"crates/docs_preprocessor",
"crates/editor",
"crates/eval",
"crates/evals",
"crates/extension",
"crates/extension_api",
"crates/extension_cli",
@@ -165,6 +164,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 +371,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 +539,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 +604,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 +695,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

@@ -134,7 +134,9 @@
"shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint"
"shift-f9": "editor::EditLogBreakpoint",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
}
},
{
@@ -630,6 +632,7 @@
"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",
@@ -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",

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",

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,79 +1,71 @@
You are a powerful agentic AI coding assistant. You operate exclusively in Zed, the world's best IDE.
You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
You are pair programming with a USER to solve their coding task.
The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.
Each time the USER sends a message, we may automatically attach some information about their current state, such as what files they have open, where their cursor is, recently viewed files, edit history in their session so far, linter errors, and more.
This information may or may not be relevant to the coding task, it is up for you to decide.
Your main goal is to follow the USER's instructions at each message.
## Communication
<communication>
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. Use \( and \) for inline math, \[ and \] for block math.
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.
</communication>
<tool_calling>
You have tools at your disposal to solve the coding task. Follow these rules regarding tool calls:
1. ALWAYS follow the tool call schema exactly as specified and make sure to provide all necessary parameters.
2. The conversation may reference tools that are no longer available. NEVER call tools that are not explicitly provided.
3. **NEVER refer to tool names when speaking to the USER.** For example, instead of saying 'I need to use the edit_file tool to edit your file', just say 'I will edit your file'.
4. Only calls tools when they are necessary. If the USER's task is general or you already know the answer, just respond without calling tools.
5. Before calling each tool, first explain to the USER why you are calling it.
</tool_calling>
## Searching and Reading
<search_and_reading>
If you are unsure about the answer to the USER's request or how to satiate their request, you should gather more information.
This can be done with additional tool calls, asking clarifying questions, etc...
If you are unsure about the answer to the user's request or how to satiate their request, you should gather more information.
This can be done with additional tool calls, asking clarifying questions, etc.
For example, if you've performed a semantic search, and the results may not fully answer the USER's request, or merit gathering more information, feel free to call more tools.
Similarly, if you've performed an edit that may partially satiate the USER's query, but you're not confident, gather more information or use more tools
before ending your turn.
For example, if you've performed a semantic search, and the results may not fully answer the user's request, or merit gathering more information, feel free to call more tools. Similarly, if you've performed an edit that may partially
satiate the user's query, but you're not confident, gather more information or use more tools before ending your turn.
Bias towards not asking the user for help if you can find the answer yourself.
</search_and_reading>
<making_code_changes>
When making code changes, NEVER output code to the USER, unless requested. Instead use one of the code edit tools to implement the change.
Use the code edit tools at most once per turn.
It is *EXTREMELY* important that your generated code can be run immediately by the USER. To ensure this, follow these instructions carefully:
1. Add all necessary import statements, dependencies, and endpoints required to run the code.
2. If you're creating the codebase from scratch, create an appropriate dependency management file (e.g. requirements.txt) with package versions and a helpful README.
3. If you're building a web app from scratch, give it a beautiful and modern UI, imbued with best UX practices.
4. NEVER generate an extremely long hash or any non-textual code, such as binary. These are not helpful to the USER and are very expensive.
5. Unless you are appending some small easy to apply edit to a file, or creating a new file, you MUST read the the contents or section of what you're editing before editing it.
6. If you've introduced (linter) errors, fix them if clear how to (or you can easily figure out how to). Do not make uneducated guesses. And DO NOT loop more than 3 times on fixing linter errors on the same file. On the third time, you should stop and ask the user what to do next.
7. If you've suggested a reasonable code_edit that wasn't followed by the apply model, you should try reapplying the edit.
</making_code_changes>
## Tool Use
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.
## 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
<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.
</debugging>
<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)
</calling_external_apis>
## 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}}
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:
{{#each worktrees}}
- `{{root_name}}` (absolute path: `{{abs_path}}`)
- `{{root_name}}`
{{/each}}
{{#if has_rules}}
There are rules that apply to these root directories:
{{#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}}}
``````
@@ -81,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

@@ -214,7 +214,14 @@
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 3,
// Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
"private_files": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
],
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
@@ -587,7 +594,6 @@
//
// Default: main
"fallback_branch_name": "main",
"scrollbar": {
// When to show the scrollbar in the git panel.
//
@@ -652,7 +658,8 @@
"path_search": true,
"read_file": true,
"regex_search": true,
"thinking": true
"thinking": true,
"web_search": true
}
},
"write": {
@@ -678,7 +685,8 @@
"rename": false,
"symbol_info": false,
"terminal": true,
"thinking": true
"thinking": true,
"web_search": true
}
}
},
@@ -713,7 +721,9 @@
// The list of language servers to use (or disable) for all languages.
//
// This is typically customized on a per-language basis.
"language_servers": ["..."],
"language_servers": [
"..."
],
// When to automatically save edited buffers. This setting can
// take four values.
//
@@ -909,7 +919,9 @@
// for files that are not tracked by git, but are still important to your project. Note that globs
// that are overly broad can slow down Zed's file scanning. `file_scan_exclusions` takes
// precedence over these inclusions.
"file_scan_inclusions": [".env*"],
"file_scan_inclusions": [
".env*"
],
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -961,7 +973,15 @@
// Any addition to this list will be merged with the default list.
// Globs are matched relative to the worktree root,
// except when starting with a slash (/) or equivalent in Windows.
"disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"],
"disabled_globs": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/.dev.vars",
"**/secrets.yml"
],
// When to show edit predictions previews in buffer.
// This setting takes two possible values:
// 1. Display predictions inline when there are no language server completions available.
@@ -1094,7 +1114,12 @@
// Default directories to search for virtual environments, relative
// to the current working directory. We recommend overriding this
// in your project's settings, rather than globally.
"directories": [".env", "env", ".venv", "venv"],
"directories": [
".env",
"env",
".venv",
"venv"
],
// Can also be `csh`, `fish`, `nushell` and `power_shell`
"activate_script": "default"
}
@@ -1158,8 +1183,15 @@
// }
//
"file_types": {
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
"Shell Script": [".env.*"]
"JSONC": [
"**/.zed/**/*.json",
"**/zed/**/*.json",
"**/Zed/**/*.json",
"**/.vscode/**/*.json"
],
"Shell Script": [
".env.*"
]
},
// By default use a recent system version of node, or install our own.
// You can override this to use a version of node that is not in $PATH with:
@@ -1232,10 +1264,15 @@
// Different settings for specific languages.
"languages": {
"Astro": {
"language_servers": ["astro-language-server", "..."],
"language_servers": [
"astro-language-server",
"..."
],
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-astro"]
"plugins": [
"prettier-plugin-astro"
]
}
},
"Blade": {
@@ -1271,10 +1308,19 @@
"ensure_final_newline_on_save": false
},
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
},
"Erlang": {
"language_servers": ["erlang-ls", "!elp", "..."]
"language_servers": [
"erlang-ls",
"!elp",
"..."
]
},
"Git Commit": {
"allow_rewrap": "anywhere"
@@ -1290,7 +1336,12 @@
}
},
"HEEX": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
},
"HTML": {
"prettier": {
@@ -1300,11 +1351,17 @@
"Java": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-java"]
"plugins": [
"prettier-plugin-java"
]
}
},
"JavaScript": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"language_servers": [
"!typescript-language-server",
"vtsls",
"..."
],
"prettier": {
"allowed": true
}
@@ -1322,7 +1379,10 @@
"LaTeX": {
"format_on_save": "on",
"formatter": "language_server",
"language_servers": ["texlab", "..."],
"language_servers": [
"texlab",
"..."
],
"prettier": {
"allowed": false
}
@@ -1337,10 +1397,16 @@
}
},
"PHP": {
"language_servers": ["phpactor", "!intelephense", "..."],
"language_servers": [
"phpactor",
"!intelephense",
"..."
],
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-php"],
"plugins": [
"@prettier/plugin-php"
],
"parser": "php"
}
},
@@ -1348,7 +1414,12 @@
"allow_rewrap": "anywhere"
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "..."]
"language_servers": [
"solargraph",
"!ruby-lsp",
"!rubocop",
"..."
]
},
"SCSS": {
"prettier": {
@@ -1358,21 +1429,36 @@
"SQL": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-sql"]
"plugins": [
"prettier-plugin-sql"
]
}
},
"Starlark": {
"language_servers": ["starpls", "!buck2-lsp", "..."]
"language_servers": [
"starpls",
"!buck2-lsp",
"..."
]
},
"Svelte": {
"language_servers": ["svelte-language-server", "..."],
"language_servers": [
"svelte-language-server",
"..."
],
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-svelte"]
"plugins": [
"prettier-plugin-svelte"
]
}
},
"TSX": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"language_servers": [
"!typescript-language-server",
"vtsls",
"..."
],
"prettier": {
"allowed": true
}
@@ -1383,13 +1469,20 @@
}
},
"TypeScript": {
"language_servers": ["!typescript-language-server", "vtsls", "..."],
"language_servers": [
"!typescript-language-server",
"vtsls",
"..."
],
"prettier": {
"allowed": true
}
},
"Vue.js": {
"language_servers": ["vue-language-server", "..."],
"language_servers": [
"vue-language-server",
"..."
],
"prettier": {
"allowed": true
}
@@ -1397,7 +1490,9 @@
"XML": {
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-xml"]
"plugins": [
"@prettier/plugin-xml"
]
}
},
"YAML": {
@@ -1406,7 +1501,10 @@
}
},
"Zig": {
"language_servers": ["zls", "..."]
"language_servers": [
"zls",
"..."
]
}
},
// Different settings for specific language models.
@@ -1560,7 +1658,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,
};
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>,
@@ -497,12 +503,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 +517,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 +599,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 +689,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 +737,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 +762,10 @@ impl ActiveThread {
this
}
pub fn context_store(&self) -> &Entity<ContextStore> {
&self.context_store
}
pub fn thread(&self) -> &Entity<Thread> {
&self.thread
}
@@ -780,6 +796,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 +884,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 +973,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 +1155,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,6 +1279,7 @@ impl ActiveThread {
}
self.thread.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model.model, RequestKind::Chat, cx)
});
cx.notify();
@@ -1402,16 +1511,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 +1728,9 @@ impl ActiveThread {
this.pt_4()
}
})
.pb_4()
.pl_2()
.pr_2p5()
.pb_4()
.child(
v_flex()
.bg(colors.editor_background)
@@ -1675,6 +1791,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 +1837,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 +1855,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 +2027,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 +2065,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 +2094,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 +2447,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 +2515,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 +2545,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 +2612,7 @@ impl ActiveThread {
open_markdown_link(text, workspace.clone(), window, cx);
}
})
.into_any_element()
})),
),
),
@@ -2468,11 +2650,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 +2675,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 +2725,7 @@ impl ActiveThread {
)
} else {
v_flex()
.my_3()
.mb_2()
.rounded_lg()
.border_1()
.border_color(self.tool_card_border_color(cx))
@@ -2761,7 +2942,7 @@ impl ActiveThread {
)
})
}
})
}).into_any_element()
}
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
@@ -2771,53 +2952,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 +3197,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 +3278,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 +3313,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

@@ -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(
@@ -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

@@ -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,13 +38,14 @@ 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,
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,16 +1120,16 @@ 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()
.action(
"Install MCPs",
zed_actions::Extensions {
Box::new(zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::ContextServers,
),
}
.boxed_clone(),
}),
)
},
))
@@ -1131,6 +1139,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 +1543,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 +1567,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 +1672,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 +1779,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 +1807,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 +1879,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 +1910,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;
@@ -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,
}
}
@@ -256,6 +271,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 +293,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,6 +330,7 @@ impl MessageEditor {
// Send to model after summaries are done
thread
.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model, request_kind, cx);
})
.log_err();
@@ -937,6 +971,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 +1059,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 +1108,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;
@@ -68,6 +70,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);
@@ -226,7 +246,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 +292,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,6 +307,7 @@ 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>,
@@ -294,6 +342,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,
@@ -313,6 +362,7 @@ 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,
@@ -367,6 +417,7 @@ impl Thread {
})
.collect(),
next_message_id,
last_prompt_id: PromptId::new(),
context: BTreeMap::default(),
context_by_message: HashMap::default(),
project_context,
@@ -381,6 +432,7 @@ 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,
@@ -414,6 +466,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()
}
@@ -643,10 +699,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>,
@@ -658,10 +734,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() {
@@ -689,6 +764,12 @@ impl Thread {
cx,
);
}
AssistantContext::Excerpt(excerpt_context) => {
log.buffer_added_as_context(
excerpt_context.context_buffer.buffer.clone(),
cx,
);
}
AssistantContext::FetchedUrl(_) | AssistantContext::Thread(_) => {}
}
}
@@ -841,6 +922,7 @@ 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(),
})
@@ -895,9 +977,11 @@ impl Thread {
pub fn to_completion_request(
&self,
request_kind: RequestKind,
cx: &App,
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(),
@@ -905,20 +989,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 {
@@ -999,15 +1096,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,
@@ -1026,22 +1114,30 @@ impl Thread {
cx: &mut Context<Self>,
) {
let pending_completion_id = post_inc(&mut self.completion_count);
let request_callback_parameters = if self.request_callback.is_some() {
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 request_callback_parameters = request_callback_parameters;
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
@@ -1063,6 +1159,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;
@@ -1150,7 +1247,7 @@ impl Thread {
}
})?;
anyhow::Ok((stop_reason, request_callback_parameters))
anyhow::Ok(stop_reason)
};
let result = stream_completion.await;
@@ -1159,24 +1256,14 @@ impl Thread {
.update(cx, |thread, cx| {
thread.finalize_pending_checkpoint(cx);
match result.as_ref() {
Ok((stop_reason, request_callback_parameters)) => {
match stop_reason {
StopReason::ToolUse => {
let tool_uses = thread.use_pending_tools(cx);
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
let tool_uses = thread.use_pending_tools(cx);
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
}
if let Some((request_callback, (request, response_events))) = thread
.request_callback
.as_mut()
.zip(request_callback_parameters.as_ref())
{
request_callback(request, response_events);
}
}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
},
Err(error) => {
if error.is::<PaymentRequiredError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
@@ -1184,6 +1271,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>()
{
@@ -1213,9 +1306,15 @@ impl Thread {
thread.cancel_last_completion(cx);
}
}
cx.emit(ThreadEvent::Stopped(
result.map(|result| result.0).map_err(Arc::new),
));
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);
@@ -1225,6 +1324,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,
@@ -1267,8 +1367,15 @@ impl Thread {
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 {
@@ -1455,6 +1562,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;
@@ -1497,17 +1610,11 @@ 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>) {
// TODO: Don't insert a dummy user message here. Ensure this works with the thinking model.
// 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,
);
self.insert_user_message("Here are the tool results.", Vec::new(), None, cx);
}
/// Cancels the last pending completion, if there are any pending.
@@ -1837,14 +1944,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)
})
}
@@ -1904,6 +2011,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 {
@@ -1917,30 +2053,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(
@@ -1965,6 +2104,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,
@@ -1975,6 +2116,7 @@ pub enum ThreadError {
#[derive(Debug, Clone)]
pub enum ThreadEvent {
ShowError(ThreadError),
UsageUpdated(RequestUsage),
StreamedCompletion,
StreamedAssistantText(MessageId, String),
StreamedAssistantThinking(MessageId, String),
@@ -2080,7 +2222,7 @@ fn main() {{
assert_eq!(message.context, expected_context);
// Check message in request
let request = thread.read_with(cx, |thread, cx| {
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
@@ -2172,7 +2314,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| {
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
@@ -2224,7 +2366,7 @@ fn main() {{
assert_eq!(message.context, "");
// Check message in request
let request = thread.read_with(cx, |thread, cx| {
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
@@ -2244,7 +2386,7 @@ fn main() {{
assert_eq!(message2.context, "");
// Check that both messages appear in the request
let request = thread.read_with(cx, |thread, cx| {
let request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
@@ -2286,7 +2428,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| {
let initial_request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
@@ -2316,7 +2458,7 @@ fn main() {{
});
// Create a new request and check for the stale buffer warning
let new_request = thread.read_with(cx, |thread, cx| {
let new_request = thread.update(cx, |thread, cx| {
thread.to_completion_request(RequestKind::Chat, cx)
});
@@ -2345,6 +2487,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);
@@ -2384,7 +2527,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>,
@@ -597,6 +697,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(),
)
}
}

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

@@ -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

@@ -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,6 +9,10 @@ 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;
use language_model::LanguageModelRequestMessage;
@@ -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,6 +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

@@ -22,14 +22,17 @@ 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;
@@ -78,6 +81,17 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
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

@@ -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,268 @@
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 FindReplaceFileToolInput {
/// The path of the file to modify.
///
/// 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 unique string to find in the file. This string cannot be empty;
/// if the string is empty, the tool call will fail. Remember, do not use this tool
/// to create new files from scratch, or to overwrite existing files! Use a different
/// approach if you want to do that.
///
/// If this string appears more than once in the file, this tool call will fail,
/// so it is absolutely critical that you verify ahead of time that the string
/// is unique. You can search within the file to verify this.
///
/// To make the string more likely to be unique, include a minimum of 3 lines of context
/// before the string you actually want to find, as well as a minimum of 3 lines of
/// context after the string you want to find. (These lines of context should appear
/// in the `replace` string as well.) If 3 lines of context is not enough to obtain
/// a string that appears only once in the file, then double the number of context lines
/// until the string becomes unique. (Start with 3 lines before and 3 lines after
/// though, because too much context is needlessly costly.)
///
/// Do not alter the context lines of code in any way, and make sure to preserve all
/// whitespace and indentation for all lines of code. This string must be exactly as
/// it appears in the file, because this tool will do a literal find/replace, and if
/// even one character in this string is different in any way from how it appears
/// in the file, then the tool call will fail.
///
/// If you get an error that the `find` string was not found, this means that either
/// you made a mistake, or that the file has changed since you last looked at it.
/// Either way, when this happens, you should retry doing this tool call until it
/// succeeds, up to 3 times. Each time you retry, you should take another look at
/// the exact text of the file in question, to make sure that you are searching for
/// exactly the right string. Regardless of whether it was because you made a mistake
/// or because the file changed since you last looked at it, you should be extra
/// careful when retrying in this way. It's a bad experience for the user if
/// this `find` string isn't found, so be super careful to get it exactly right!
///
/// <example>
/// If a file contains this code:
///
/// ```ignore
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
/// // Check if user exists first
/// let user = database.find_user(user_id)?;
///
/// // This is the part we want to modify
/// if user.role == "admin" {
/// return Ok(true);
/// }
///
/// // Check other permissions
/// check_custom_permissions(user_id)
/// }
/// ```
///
/// Your find string should include at least 3 lines of context before and after the part
/// you want to change:
///
/// ```ignore
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
/// // Check if user exists first
/// let user = database.find_user(user_id)?;
///
/// // This is the part we want to modify
/// if user.role == "admin" {
/// return Ok(true);
/// }
///
/// // Check other permissions
/// check_custom_permissions(user_id)
/// }
/// ```
///
/// And your replace string might look like:
///
/// ```ignore
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
/// // Check if user exists first
/// let user = database.find_user(user_id)?;
///
/// // This is the part we want to modify
/// if user.role == "admin" || user.role == "superuser" {
/// return Ok(true);
/// }
///
/// // Check other permissions
/// check_custom_permissions(user_id)
/// }
/// ```
/// </example>
pub find: String,
/// The string to replace the one unique occurrence of the find string with.
pub replace: String,
}
pub struct FindReplaceFileTool;
impl Tool for FindReplaceFileTool {
fn name(&self) -> String {
"find_replace_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("find_replace_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::Pencil
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<FindReplaceFileToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<FindReplaceFileToolInput>(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::<FindReplaceFileToolInput>(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.find.is_empty() {
return Err(anyhow!("`find` string cannot be empty. Use a different tool if you want to create a file."));
}
if input.find == input.replace {
return Err(anyhow!("The `find` and `replace` strings are identical, so no changes would be made."));
}
let result = cx
.background_spawn(async move {
// Try to match exactly
let diff = replace_exact(&input.find, &input.replace, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&input.find, &input.replace, &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 `find` string wasn't found.",
input.path.display()
)
} else {
anyhow!("Failed to match the provided `find` 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

@@ -80,18 +80,29 @@ impl Tool for PathSearchTool {
let task = search_paths(&glob, project, cx);
cx.background_spawn(async move {
let matches = task.await?;
let paginated_matches = &matches[cmp::min(offset, matches.len())..cmp::min(offset + RESULTS_PER_PAGE, matches.len())];
let mut message = format!(
"Found {} total matches. Showing results {}-{} (provide 'offset' parameter for more results):\n",
matches.len(),
offset + 1,
offset as usize + paginated_matches.len(),
);
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
write!(&mut message, "\n{}", mat.display()).unwrap();
let paginated_matches = &matches[cmp::min(offset, matches.len())
..cmp::min(offset + RESULTS_PER_PAGE, matches.len())];
if matches.is_empty() {
Ok("No matches found".to_string())
} else {
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 + paginated_matches.len()
)
.unwrap();
}
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
write!(&mut message, "\n{}", mat.display()).unwrap();
}
Ok(message)
}
Ok(message)
}).into()
})
.into()
}
}
@@ -101,7 +112,7 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
if glob.is_empty() { "*" } else { glob },
]) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))).into(),
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
};
let snapshots: Vec<Snapshot> = project
.read(cx)

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

@@ -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

@@ -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;

View File

@@ -1,5 +1,5 @@
use crate::Cents;
use crate::db::user;
use crate::db::{billing_subscription, user};
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
use crate::{Config, db::billing_preference};
use anyhow::{Result, anyhow};
@@ -8,7 +8,9 @@ use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use thiserror::Error;
use util::maybe;
use uuid::Uuid;
use zed_llm_client::Plan;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -24,11 +26,12 @@ pub struct LlmTokenClaims {
pub is_staff: bool,
pub has_llm_closed_beta_feature_flag: bool,
pub bypass_account_age_check: bool,
pub has_predict_edits_feature_flag: bool,
pub has_llm_subscription: bool,
pub max_monthly_spend_in_cents: u32,
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
pub plan: rpc::proto::Plan,
pub plan: Plan,
#[serde(default)]
pub subscription_period: Option<(NaiveDateTime, NaiveDateTime)>,
}
const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
@@ -39,8 +42,9 @@ impl LlmTokenClaims {
is_staff: bool,
billing_preferences: Option<billing_preference::Model>,
feature_flags: &Vec<String>,
has_llm_subscription: bool,
has_legacy_llm_subscription: bool,
plan: rpc::proto::Plan,
subscription: Option<billing_subscription::Model>,
system_id: Option<String>,
config: &Config,
) -> Result<String> {
@@ -66,10 +70,7 @@ impl LlmTokenClaims {
bypass_account_age_check: feature_flags
.iter()
.any(|flag| flag == "bypass-account-age-check"),
has_predict_edits_feature_flag: feature_flags
.iter()
.any(|flag| flag == "predict-edits"),
has_llm_subscription,
has_llm_subscription: has_legacy_llm_subscription,
max_monthly_spend_in_cents: billing_preferences
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {
preferences.max_monthly_llm_usage_spending_in_cents as u32
@@ -77,7 +78,18 @@ impl LlmTokenClaims {
custom_llm_monthly_allowance_in_cents: user
.custom_llm_monthly_allowance_in_cents
.map(|allowance| allowance as u32),
plan,
plan: match plan {
rpc::proto::Plan::Free => Plan::Free,
rpc::proto::Plan::ZedPro => Plan::ZedPro,
rpc::proto::Plan::ZedProTrial => Plan::ZedProTrial,
},
subscription_period: maybe!({
let subscription = subscription?;
let period_start_at = subscription.current_period_start_at()?;
let period_end_at = subscription.current_period_end_at()?;
Some((period_start_at.naive_utc(), period_end_at.naive_utc()))
}),
};
Ok(jsonwebtoken::encode(

View File

@@ -3707,7 +3707,9 @@ async fn count_language_model_tokens(
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
proto::Plan::ZedPro => Box::new(ZedProCountLanguageModelTokensRateLimit),
proto::Plan::Free => Box::new(FreeCountLanguageModelTokensRateLimit),
proto::Plan::Free | proto::Plan::ZedProTrial => {
Box::new(FreeCountLanguageModelTokensRateLimit)
}
};
session
@@ -3827,7 +3829,7 @@ async fn compute_embeddings(
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
proto::Plan::ZedPro => Box::new(ZedProComputeEmbeddingsRateLimit),
proto::Plan::Free => Box::new(FreeComputeEmbeddingsRateLimit),
proto::Plan::Free | proto::Plan::ZedProTrial => Box::new(FreeComputeEmbeddingsRateLimit),
};
session
@@ -4135,7 +4137,8 @@ async fn get_llm_api_token(
Err(anyhow!("terms of service not accepted"))?
}
let has_llm_subscription = session.has_llm_subscription(&db).await?;
let has_legacy_llm_subscription = session.has_llm_subscription(&db).await?;
let billing_subscription = db.get_active_billing_subscription(user.id).await?;
let billing_preferences = db.get_billing_preferences(user.id).await?;
let token = LlmTokenClaims::create(
@@ -4143,8 +4146,9 @@ async fn get_llm_api_token(
session.is_staff(),
billing_preferences,
&flags,
has_llm_subscription,
has_legacy_llm_subscription,
session.current_plan(&db).await?,
billing_subscription,
session.system_id.clone(),
&session.app_state.config,
)?;

View File

@@ -1,16 +1,16 @@
use std::sync::Arc;
use crate::{Cents, Result, llm};
use anyhow::{Context as _, anyhow};
use anyhow::Context as _;
use chrono::{Datelike, Utc};
use collections::HashMap;
use serde::{Deserialize, Serialize};
use stripe::PriceId;
use tokio::sync::RwLock;
pub struct StripeBilling {
state: RwLock<StripeBillingState>,
client: Arc<stripe::Client>,
zed_pro_price_id: Option<String>,
}
#[derive(Default)]
@@ -32,11 +32,10 @@ struct StripeBillingPrice {
}
impl StripeBilling {
pub fn new(client: Arc<stripe::Client>, zed_pro_price_id: Option<String>) -> Self {
pub fn new(client: Arc<stripe::Client>) -> Self {
Self {
client,
state: RwLock::default(),
zed_pro_price_id,
}
}
@@ -385,23 +384,45 @@ impl StripeBilling {
Ok(session.url.context("no checkout session URL")?)
}
pub async fn checkout_with_zed_pro(
pub async fn checkout_with_price(
&self,
price_id: PriceId,
customer_id: stripe::CustomerId,
github_login: &str,
success_url: &str,
) -> Result<String> {
let zed_pro_price_id = self
.zed_pro_price_id
.as_ref()
.ok_or_else(|| anyhow!("Zed Pro price ID not set"))?;
let mut params = stripe::CreateCheckoutSession::new();
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
params.customer = Some(customer_id);
params.client_reference_id = Some(github_login);
params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
price: Some(zed_pro_price_id.clone()),
price: Some(price_id.to_string()),
quantity: Some(1),
..Default::default()
}]);
params.success_url = Some(success_url);
let session = stripe::CheckoutSession::create(&self.client, params).await?;
Ok(session.url.context("no checkout session URL")?)
}
pub async fn checkout_with_zed_pro_trial(
&self,
zed_pro_price_id: PriceId,
customer_id: stripe::CustomerId,
github_login: &str,
success_url: &str,
) -> Result<String> {
let mut params = stripe::CreateCheckoutSession::new();
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
trial_period_days: Some(14),
..Default::default()
});
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
params.customer = Some(customer_id);
params.client_reference_id = Some(github_login);
params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
price: Some(zed_pro_price_id.to_string()),
quantity: Some(1),
..Default::default()
}]);

View File

@@ -694,15 +694,7 @@ async fn test_collaborating_with_code_actions(
// Confirming the code action will trigger a resolve request.
let confirm_action = editor_b
.update_in(cx_b, |editor, window, cx| {
Editor::confirm_code_action(
editor,
&ConfirmCodeAction {
item_ix: Some(0),
from_mouse_context_menu: false,
},
window,
cx,
)
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx)
})
.unwrap();
fake_language_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(

View File

@@ -558,6 +558,8 @@ impl TestServer {
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,

View File

@@ -201,6 +201,7 @@ pub fn components() -> AllComponents {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ComponentScope {
Agent,
Collaboration,
DataDisplay,
Editor,
@@ -220,6 +221,7 @@ pub enum ComponentScope {
impl Display for ComponentScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ComponentScope::Agent => write!(f, "Agent"),
ComponentScope::Collaboration => write!(f, "Collaboration"),
ComponentScope::DataDisplay => write!(f, "Data Display"),
ComponentScope::Editor => write!(f, "Editor"),

View File

@@ -41,6 +41,10 @@ pub enum Model {
O1,
#[serde(alias = "o1-mini", rename = "o3-mini")]
O3Mini,
#[serde(alias = "o3", rename = "o3")]
O3,
#[serde(alias = "o4-mini", rename = "o4-mini")]
O4Mini,
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
Claude3_5Sonnet,
#[serde(alias = "claude-3-7-sonnet", rename = "claude-3.7-sonnet")]
@@ -63,6 +67,8 @@ impl Model {
| Self::Gpt4
| Self::Gpt4_1
| Self::Gpt3_5Turbo
| Self::O3
| Self::O4Mini
| Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking => true,
@@ -78,6 +84,8 @@ impl Model {
"gpt-3.5-turbo" => Ok(Self::Gpt3_5Turbo),
"o1" => Ok(Self::O1),
"o3-mini" => Ok(Self::O3Mini),
"o3" => Ok(Self::O3),
"o4-mini" => Ok(Self::O4Mini),
"claude-3-5-sonnet" => Ok(Self::Claude3_5Sonnet),
"claude-3-7-sonnet" => Ok(Self::Claude3_7Sonnet),
"claude-3.7-sonnet-thought" => Ok(Self::Claude3_7SonnetThinking),
@@ -95,6 +103,8 @@ impl Model {
Self::Gpt4o => "gpt-4o",
Self::O3Mini => "o3-mini",
Self::O1 => "o1",
Self::O3 => "o3",
Self::O4Mini => "o4-mini",
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
Self::Claude3_7Sonnet => "claude-3-7-sonnet",
Self::Claude3_7SonnetThinking => "claude-3.7-sonnet-thought",
@@ -111,6 +121,8 @@ impl Model {
Self::Gpt4o => "GPT-4o",
Self::O3Mini => "o3-mini",
Self::O1 => "o1",
Self::O3 => "o3",
Self::O4Mini => "o4-mini",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
@@ -123,10 +135,12 @@ impl Model {
match self {
Self::Gpt4o => 64_000,
Self::Gpt4 => 32_768,
Self::Gpt4_1 => 1_047_576,
Self::Gpt4_1 => 128_000,
Self::Gpt3_5Turbo => 12_288,
Self::O3Mini => 64_000,
Self::O1 => 20_000,
Self::O3 => 128_000,
Self::O4Mini => 128_000,
Self::Claude3_5Sonnet => 200_000,
Self::Claude3_7Sonnet => 90_000,
Self::Claude3_7SonnetThinking => 90_000,

View File

@@ -1,5 +1,5 @@
use ::fs::Fs;
use anyhow::{Context as _, Ok, Result, anyhow};
use anyhow::{Context as _, Result, anyhow};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
@@ -256,7 +256,21 @@ pub trait DebugAdapter: 'static + Send + Sync {
self.name()
);
delegate.update_status(self.name(), DapStatus::Downloading);
self.install_binary(version, delegate).await?;
match self.install_binary(version, delegate).await {
Ok(_) => {
delegate.update_status(self.name(), DapStatus::None);
}
Err(error) => {
delegate.update_status(
self.name(),
DapStatus::Failed {
error: error.to_string(),
},
);
return Err(error);
}
}
delegate
.updated_adapters()

View File

@@ -7,7 +7,7 @@ pub mod transport;
pub use dap_types::*;
pub use registry::DapRegistry;
pub use task::{DebugAdapterConfig, DebugRequestType};
pub use task::DebugRequestType;
pub type ScopeId = u64;
pub type VariableReference = u64;

View File

@@ -12,8 +12,9 @@ use dap::{
};
use futures::{SinkExt as _, channel::mpsc};
use gpui::{
Action, App, AsyncWindowContext, Context, Entity, EntityId, EventEmitter, FocusHandle,
Focusable, Subscription, Task, WeakEntity, actions,
Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
actions, anchored, deferred,
};
use project::{
@@ -32,6 +33,7 @@ use std::sync::Arc;
use task::DebugTaskDefinition;
use terminal_view::terminal_panel::TerminalPanel;
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
use util::debug_panic;
use workspace::{
Workspace,
dock::{DockPosition, Panel, PanelEvent},
@@ -64,6 +66,7 @@ pub struct DebugPanel {
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
_subscriptions: Vec<Subscription>,
}
@@ -126,6 +129,7 @@ impl DebugPanel {
focus_handle: cx.focus_handle(),
project: project.downgrade(),
workspace: workspace.weak_handle(),
context_menu: None,
};
debug_panel
@@ -313,8 +317,20 @@ impl DebugPanel {
.any(|item| item.read(cx).session_id(cx) == session_id)
{
// We already have an item for this session.
debug_panic!("We should never reuse session ids");
return;
}
this.sessions.retain(|session| {
session
.read(cx)
.mode()
.as_running()
.map_or(false, |running_state| {
!running_state.read(cx).session().read(cx).is_terminated()
})
});
let session_item = DebugSession::running(
project,
this.workspace.clone(),
@@ -438,7 +454,13 @@ impl DebugPanel {
else {
return;
};
session.update(cx, |this, cx| {
if let Some(running) = this.mode().as_running() {
running.update(cx, |this, cx| {
this.serialize_layout(window, cx);
});
}
});
let session_id = session.update(cx, |this, cx| this.session_id(cx));
let should_prompt = self
.project
@@ -567,6 +589,57 @@ impl DebugPanel {
)
}
fn deploy_context_menu(
&mut self,
position: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(running_state) = self
.active_session
.as_ref()
.and_then(|session| session.read(cx).mode().as_running().cloned())
{
let pane_items_status = running_state.read(cx).pane_items_status(cx);
let this = cx.weak_entity();
let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
for (item_kind, is_visible) in pane_items_status.into_iter() {
menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, {
let this = this.clone();
move |window, cx| {
this.update(cx, |this, cx| {
if let Some(running_state) =
this.active_session.as_ref().and_then(|session| {
session.read(cx).mode().as_running().cloned()
})
{
running_state.update(cx, |state, cx| {
if is_visible {
state.remove_pane_item(item_kind, window, cx);
} else {
state.add_pane_item(item_kind, position, window, cx);
}
})
}
})
.ok();
}
});
}
menu
});
window.focus(&context_menu.focus_handle(cx));
let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
this.context_menu.take();
cx.notify();
});
self.context_menu = Some((context_menu, position, subscription));
}
}
fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
let active_session = self.active_session.clone();
@@ -709,9 +782,6 @@ impl DebugPanel {
this.restart_session(cx);
},
))
.disabled(
!capabilities.supports_restart_request.unwrap_or_default(),
)
.tooltip(move |window, cx| {
Tooltip::text("Restart")(window, cx)
}),
@@ -891,11 +961,49 @@ impl Render for DebugPanel {
let has_sessions = self.sessions.len() > 0;
debug_assert_eq!(has_sessions, self.active_session.is_some());
if self
.active_session
.as_ref()
.and_then(|session| session.read(cx).mode().as_running().cloned())
.map(|state| state.read(cx).has_open_context_menu(cx))
.unwrap_or(false)
{
self.context_menu.take();
}
v_flex()
.size_full()
.key_context("DebugPanel")
.child(h_flex().children(self.top_controls_strip(window, cx)))
.track_focus(&self.focus_handle(cx))
.when(self.active_session.is_some(), |this| {
this.on_mouse_down(
MouseButton::Right,
cx.listener(|this, event: &MouseDownEvent, window, cx| {
if this
.active_session
.as_ref()
.and_then(|session| {
session.read(cx).mode().as_running().map(|state| {
state.read(cx).has_pane_at_position(event.position)
})
})
.unwrap_or(false)
{
this.deploy_context_menu(event.position, window, cx);
}
}),
)
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
deferred(
anchored()
.position(*position)
.anchor(gpui::Corner::TopLeft)
.child(menu.clone()),
)
.with_priority(1)
}))
})
.map(|this| {
if has_sessions {
this.children(self.active_session.clone())

View File

@@ -1,4 +1,5 @@
use collections::HashMap;
use dap::Capabilities;
use db::kvp::KEY_VALUE_STORE;
use gpui::{Axis, Context, Entity, EntityId, Focusable, Subscription, WeakEntity, Window};
use project::Project;
@@ -9,19 +10,43 @@ use workspace::{Member, Pane, PaneAxis, Workspace};
use crate::session::running::{
self, RunningState, SubView, breakpoint_list::BreakpointList, console::Console,
module_list::ModuleList, stack_frame_list::StackFrameList, variable_list::VariableList,
loaded_source_list::LoadedSourceList, module_list::ModuleList,
stack_frame_list::StackFrameList, variable_list::VariableList,
};
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Clone, Hash, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) enum DebuggerPaneItem {
Console,
Variables,
BreakpointList,
Frames,
Modules,
LoadedSources,
}
impl DebuggerPaneItem {
pub(crate) fn all() -> &'static [DebuggerPaneItem] {
static VARIANTS: &[DebuggerPaneItem] = &[
DebuggerPaneItem::Console,
DebuggerPaneItem::Variables,
DebuggerPaneItem::BreakpointList,
DebuggerPaneItem::Frames,
DebuggerPaneItem::Modules,
DebuggerPaneItem::LoadedSources,
];
VARIANTS
}
pub(crate) fn is_supported(&self, capabilities: &Capabilities) -> bool {
match self {
DebuggerPaneItem::Modules => capabilities.supports_modules_request.unwrap_or_default(),
DebuggerPaneItem::LoadedSources => capabilities
.supports_loaded_sources_request
.unwrap_or_default(),
_ => true,
}
}
pub(crate) fn to_shared_string(self) -> SharedString {
match self {
DebuggerPaneItem::Console => SharedString::new_static("Console"),
@@ -29,10 +54,17 @@ impl DebuggerPaneItem {
DebuggerPaneItem::BreakpointList => SharedString::new_static("Breakpoints"),
DebuggerPaneItem::Frames => SharedString::new_static("Frames"),
DebuggerPaneItem::Modules => SharedString::new_static("Modules"),
DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"),
}
}
}
impl From<DebuggerPaneItem> for SharedString {
fn from(item: DebuggerPaneItem) -> Self {
item.to_shared_string()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct SerializedAxis(pub Axis);
@@ -136,6 +168,7 @@ pub(crate) fn deserialize_pane_layout(
module_list: &Entity<ModuleList>,
console: &Entity<Console>,
breakpoint_list: &Entity<BreakpointList>,
loaded_sources: &Entity<LoadedSourceList>,
subscriptions: &mut HashMap<EntityId, Subscription>,
window: &mut Window,
cx: &mut Context<RunningState>,
@@ -157,6 +190,7 @@ pub(crate) fn deserialize_pane_layout(
module_list,
console,
breakpoint_list,
loaded_sources,
subscriptions,
window,
cx,
@@ -191,7 +225,7 @@ pub(crate) fn deserialize_pane_layout(
.iter()
.map(|child| match child {
DebuggerPaneItem::Frames => Box::new(SubView::new(
pane.focus_handle(cx),
stack_frame_list.focus_handle(cx),
stack_frame_list.clone().into(),
DebuggerPaneItem::Frames,
None,
@@ -212,13 +246,19 @@ pub(crate) fn deserialize_pane_layout(
cx,
)),
DebuggerPaneItem::Modules => Box::new(SubView::new(
pane.focus_handle(cx),
module_list.focus_handle(cx),
module_list.clone().into(),
DebuggerPaneItem::Modules,
None,
cx,
)),
DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
loaded_sources.focus_handle(cx),
loaded_sources.clone().into(),
DebuggerPaneItem::LoadedSources,
None,
cx,
)),
DebuggerPaneItem::Console => Box::new(SubView::new(
pane.focus_handle(cx),
console.clone().into(),

View File

@@ -11,12 +11,12 @@ use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout};
use super::DebugPanelItemEvent;
use breakpoint_list::BreakpointList;
use collections::HashMap;
use collections::{HashMap, IndexMap};
use console::Console;
use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
use gpui::{
Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
NoAction, Subscription, Task, WeakEntity,
NoAction, Pixels, Point, Subscription, Task, WeakEntity,
};
use loaded_source_list::LoadedSourceList;
use module_list::ModuleList;
@@ -49,8 +49,10 @@ pub struct RunningState {
variable_list: Entity<variable_list::VariableList>,
_subscriptions: Vec<Subscription>,
stack_frame_list: Entity<stack_frame_list::StackFrameList>,
_module_list: Entity<module_list::ModuleList>,
loaded_sources_list: Entity<LoadedSourceList>,
module_list: Entity<module_list::ModuleList>,
_console: Entity<Console>,
breakpoint_list: Entity<BreakpointList>,
panes: PaneGroup,
pane_close_subscriptions: HashMap<EntityId, Subscription>,
_schedule_serialize: Option<Task<()>>,
@@ -383,7 +385,6 @@ impl RunningState {
let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
#[expect(unused)]
let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
let console = cx.new(|cx| {
@@ -396,7 +397,7 @@ impl RunningState {
)
});
let breakpoints = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
let breakpoint_list = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
let _subscriptions = vec![
cx.observe(&module_list, |_, _, cx| cx.notify()),
@@ -421,6 +422,9 @@ impl RunningState {
}
cx.notify()
}),
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
this.serialize_layout(window, cx);
}),
];
let mut pane_close_subscriptions = HashMap::default();
@@ -433,7 +437,8 @@ impl RunningState {
&variable_list,
&module_list,
&console,
&breakpoints,
&breakpoint_list,
&loaded_source_list,
&mut pane_close_subscriptions,
window,
cx,
@@ -442,14 +447,37 @@ impl RunningState {
workspace::PaneGroup::with_root(root)
} else {
pane_close_subscriptions.clear();
let module_list = if session
.read(cx)
.capabilities()
.supports_modules_request
.unwrap_or(false)
{
Some(&module_list)
} else {
None
};
let loaded_source_list = if session
.read(cx)
.capabilities()
.supports_loaded_sources_request
.unwrap_or(false)
{
Some(&loaded_source_list)
} else {
None
};
let root = Self::default_pane_layout(
project,
&workspace,
&stack_frame_list,
&variable_list,
&module_list,
module_list,
loaded_source_list,
&console,
breakpoints,
&breakpoint_list,
&mut pane_close_subscriptions,
window,
cx,
@@ -469,14 +497,140 @@ impl RunningState {
stack_frame_list,
session_id,
panes,
_module_list: module_list,
module_list,
_console: console,
breakpoint_list,
loaded_sources_list: loaded_source_list,
pane_close_subscriptions,
_schedule_serialize: None,
}
}
fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
pub(crate) fn remove_pane_item(
&mut self,
item_kind: DebuggerPaneItem,
window: &mut Window,
cx: &mut Context<Self>,
) {
debug_assert!(
item_kind.is_supported(self.session.read(cx).capabilities()),
"We should only allow removing supported item kinds"
);
if let Some((pane, item_id)) = self.panes.panes().iter().find_map(|pane| {
Some(pane).zip(
pane.read(cx)
.items()
.find(|item| {
item.act_as::<SubView>(cx)
.is_some_and(|view| view.read(cx).kind == item_kind)
})
.map(|item| item.item_id()),
)
}) {
pane.update(cx, |pane, cx| {
pane.remove_item(item_id, false, true, window, cx)
})
}
}
pub(crate) fn has_pane_at_position(&self, position: Point<Pixels>) -> bool {
self.panes.pane_at_pixel_position(position).is_some()
}
pub(crate) fn add_pane_item(
&mut self,
item_kind: DebuggerPaneItem,
position: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
debug_assert!(
item_kind.is_supported(self.session.read(cx).capabilities()),
"We should only allow adding supported item kinds"
);
if let Some(pane) = self.panes.pane_at_pixel_position(position) {
let sub_view = match item_kind {
DebuggerPaneItem::Console => {
let weak_console = self._console.clone().downgrade();
Box::new(SubView::new(
pane.focus_handle(cx),
self._console.clone().into(),
item_kind,
Some(Box::new(move |cx| {
weak_console
.read_with(cx, |console, cx| console.show_indicator(cx))
.unwrap_or_default()
})),
cx,
))
}
DebuggerPaneItem::Variables => Box::new(SubView::new(
self.variable_list.focus_handle(cx),
self.variable_list.clone().into(),
item_kind,
None,
cx,
)),
DebuggerPaneItem::BreakpointList => Box::new(SubView::new(
self.breakpoint_list.focus_handle(cx),
self.breakpoint_list.clone().into(),
item_kind,
None,
cx,
)),
DebuggerPaneItem::Frames => Box::new(SubView::new(
self.stack_frame_list.focus_handle(cx),
self.stack_frame_list.clone().into(),
item_kind,
None,
cx,
)),
DebuggerPaneItem::Modules => Box::new(SubView::new(
self.module_list.focus_handle(cx),
self.module_list.clone().into(),
item_kind,
None,
cx,
)),
DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
self.loaded_sources_list.focus_handle(cx),
self.loaded_sources_list.clone().into(),
item_kind,
None,
cx,
)),
};
pane.update(cx, |pane, cx| {
pane.add_item(sub_view, false, false, None, window, cx);
})
}
}
pub(crate) fn pane_items_status(&self, cx: &App) -> IndexMap<DebuggerPaneItem, bool> {
let caps = self.session.read(cx).capabilities();
let mut pane_item_status = IndexMap::from_iter(
DebuggerPaneItem::all()
.iter()
.filter(|kind| kind.is_supported(&caps))
.map(|kind| (*kind, false)),
);
self.panes.panes().iter().for_each(|pane| {
pane.read(cx)
.items()
.filter_map(|item| item.act_as::<SubView>(cx))
.for_each(|view| {
pane_item_status.insert(view.read(cx).kind, true);
});
});
pane_item_status
}
pub(crate) fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self._schedule_serialize.is_none() {
self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
@@ -530,6 +684,10 @@ impl RunningState {
}
}
pub(crate) fn has_open_context_menu(&self, cx: &App) -> bool {
self.variable_list.read(cx).has_open_context_menu()
}
pub fn session(&self) -> &Entity<Session> {
&self.session
}
@@ -554,7 +712,7 @@ impl RunningState {
#[cfg(test)]
pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
&self._module_list
&self.module_list
}
#[cfg(test)]
@@ -788,9 +946,10 @@ impl RunningState {
workspace: &WeakEntity<Workspace>,
stack_frame_list: &Entity<StackFrameList>,
variable_list: &Entity<VariableList>,
module_list: &Entity<ModuleList>,
module_list: Option<&Entity<ModuleList>>,
loaded_source_list: Option<&Entity<LoadedSourceList>>,
console: &Entity<Console>,
breakpoints: Entity<BreakpointList>,
breakpoints: &Entity<BreakpointList>,
subscriptions: &mut HashMap<EntityId, Subscription>,
window: &mut Window,
cx: &mut Context<'_, RunningState>,
@@ -814,7 +973,7 @@ impl RunningState {
this.add_item(
Box::new(SubView::new(
breakpoints.focus_handle(cx),
breakpoints.into(),
breakpoints.clone().into(),
DebuggerPaneItem::BreakpointList,
None,
cx,
@@ -828,6 +987,7 @@ impl RunningState {
this.activate_item(0, false, false, window, cx);
});
let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
center_pane.update(cx, |this, cx| {
this.add_item(
Box::new(SubView::new(
@@ -843,22 +1003,43 @@ impl RunningState {
window,
cx,
);
this.add_item(
Box::new(SubView::new(
this.focus_handle(cx),
module_list.clone().into(),
DebuggerPaneItem::Modules,
if let Some(module_list) = module_list {
this.add_item(
Box::new(SubView::new(
module_list.focus_handle(cx),
module_list.clone().into(),
DebuggerPaneItem::Modules,
None,
cx,
)),
false,
false,
None,
window,
cx,
)),
false,
false,
None,
window,
cx,
);
this.activate_item(0, false, false, window, cx);
);
this.activate_item(0, false, false, window, cx);
}
if let Some(loaded_source_list) = loaded_source_list {
this.add_item(
Box::new(SubView::new(
loaded_source_list.focus_handle(cx),
loaded_source_list.clone().into(),
DebuggerPaneItem::LoadedSources,
None,
cx,
)),
false,
false,
None,
window,
cx,
);
this.activate_item(1, false, false, window, cx);
}
});
let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
rightmost_pane.update(cx, |this, cx| {
let weak_console = console.downgrade();

View File

@@ -3,7 +3,7 @@ use project::debugger::session::{Session, SessionEvent};
use ui::prelude::*;
use util::maybe;
pub struct LoadedSourceList {
pub(crate) struct LoadedSourceList {
list: ListState,
invalidate: bool,
focus_handle: FocusHandle,

View File

@@ -194,6 +194,10 @@ impl VariableList {
}
}
pub(super) fn has_open_context_menu(&self) -> bool {
self.open_context_menu.is_some()
}
fn build_entries(&mut self, cx: &mut Context<Self>) {
let Some(stack_frame_id) = self.selected_stack_frame_id else {
return;

View File

@@ -15,7 +15,7 @@ use text::{AnchorRangeExt, Point};
use theme::ThemeSettings;
use ui::{
ActiveTheme, AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled,
Window, div, px,
Window, div,
};
use util::maybe;
@@ -166,7 +166,8 @@ impl DiagnosticBlock {
pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
let cx = &bcx.app;
let status_colors = bcx.app.theme().status();
let max_width = px(600.);
let max_width = bcx.em_width * 100.;
let (background_color, border_color) = match self.severity {
DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),

View File

@@ -46,7 +46,8 @@ use workspace::{
actions!(diagnostics, [Deploy, ToggleWarnings]);
struct IncludeWarnings(bool);
#[derive(Default)]
pub(crate) struct IncludeWarnings(bool);
impl Global for IncludeWarnings {}
pub fn init(cx: &mut App) {
@@ -209,6 +210,7 @@ impl ProjectDiagnosticsEditor {
.detach();
cx.observe_global_in::<IncludeWarnings>(window, |this, window, cx| {
this.include_warnings = cx.global::<IncludeWarnings>().0;
this.diagnostics.clear();
this.update_all_excerpts(window, cx);
})
.detach();
@@ -300,11 +302,8 @@ impl ProjectDiagnosticsEditor {
}
}
fn toggle_warnings(&mut self, _: &ToggleWarnings, window: &mut Window, cx: &mut Context<Self>) {
self.include_warnings = !self.include_warnings;
cx.set_global(IncludeWarnings(self.include_warnings));
self.update_all_excerpts(window, cx);
cx.notify();
fn toggle_warnings(&mut self, _: &ToggleWarnings, _: &mut Window, cx: &mut Context<Self>) {
cx.set_global(IncludeWarnings(!self.include_warnings));
}
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -381,7 +380,6 @@ impl ProjectDiagnosticsEditor {
Point::zero()..buffer_snapshot.max_point(),
false,
)
.filter(|d| !(d.diagnostic.is_primary && d.diagnostic.is_unnecessary))
.collect::<Vec<_>>();
let unchanged = this.update(cx, |this, _| {
if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
@@ -482,7 +480,10 @@ impl ProjectDiagnosticsEditor {
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.select_anchor_ranges([range_to_select]);
})
})
});
if this.focus_handle.is_focused(window) {
this.editor.read(cx).focus_handle(cx).focus(window);
}
}
}

View File

@@ -1,9 +1,9 @@
use super::*;
use collections::{HashMap, HashSet};
use editor::{
DisplayPoint,
DisplayPoint, InlayId,
actions::{GoToDiagnostic, GoToPreviousDiagnostic, MoveToBeginning},
display_map::DisplayRow,
display_map::{DisplayRow, Inlay},
test::{editor_content_with_blocks, editor_test_context::EditorTestContext},
};
use gpui::{TestAppContext, VisualTestContext};
@@ -620,7 +620,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
}
#[gpui::test(iterations = 20)]
async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
let operations = env::var("OPERATIONS")
@@ -779,6 +779,162 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
}
}
// similar to above, but with inlays. Used to find panics when mixing diagnostics and inlays.
#[gpui::test]
async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/test"), json!({})).await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
let mutated_diagnostics = window.build_entity(cx, |window, cx| {
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
});
mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
assert!(diagnostics.focus_handle.is_focused(window));
});
let mut next_id = 0;
let mut next_filename = 0;
let mut language_server_ids = vec![LanguageServerId(0)];
let mut updated_language_servers = HashSet::default();
let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
Default::default();
let mut next_inlay_id = 0;
for _ in 0..operations {
match rng.gen_range(0..100) {
// language server completes its diagnostic check
0..=20 if !updated_language_servers.is_empty() => {
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
log::info!("finishing diagnostic check for language server {server_id}");
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.disk_based_diagnostics_finished(server_id, cx)
});
if rng.gen_bool(0.5) {
cx.run_until_parked();
}
}
21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
if snapshot.buffer_snapshot.len() > 0 {
let position = rng.gen_range(0..snapshot.buffer_snapshot.len());
let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
log::info!(
"adding inlay at {position}/{}: {:?}",
snapshot.buffer_snapshot.len(),
snapshot.buffer_snapshot.text(),
);
editor.splice_inlays(
&[],
vec![Inlay {
id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
position: snapshot.buffer_snapshot.anchor_before(position),
text: Rope::from(format!("Test inlay {next_inlay_id}")),
}],
cx,
);
}
});
}),
// language server updates diagnostics
_ => {
let (path, server_id, diagnostics) =
match current_diagnostics.iter_mut().choose(&mut rng) {
// update existing set of diagnostics
Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
(path.clone(), *server_id, diagnostics)
}
// insert a set of diagnostics for a new path
_ => {
let path: PathBuf =
format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
let len = rng.gen_range(128..256);
let content =
RandomCharIter::new(&mut rng).take(len).collect::<String>();
fs.insert_file(&path, content.into_bytes()).await;
let server_id = match language_server_ids.iter().choose(&mut rng) {
Some(server_id) if rng.gen_bool(0.5) => *server_id,
_ => {
let id = LanguageServerId(language_server_ids.len());
language_server_ids.push(id);
id
}
};
(
path.clone(),
server_id,
current_diagnostics.entry((path, server_id)).or_default(),
)
}
};
updated_language_servers.insert(server_id);
lsp_store.update(cx, |lsp_store, cx| {
log::info!("updating diagnostics. language server {server_id} path {path:?}");
randomly_update_diagnostics_for_path(
&fs,
&path,
diagnostics,
&mut next_id,
&mut rng,
);
lsp_store
.update_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
lsp::Url::parse("file:///test/fallback.rs").unwrap()
}),
diagnostics: diagnostics.clone(),
version: None,
},
&[],
cx,
)
.unwrap()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
}
}
log::info!("updating mutated diagnostics view");
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.update_stale_excerpts(window, cx)
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
#[gpui::test]
async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
init_test(cx);

View File

@@ -9,7 +9,7 @@ use language::Diagnostic;
use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
use crate::{Deploy, ProjectDiagnosticsEditor};
use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
pub struct DiagnosticIndicator {
summary: project::DiagnosticSummary,
@@ -94,6 +94,11 @@ impl Render for DiagnosticIndicator {
})
.on_click(cx.listener(|this, _, window, cx| {
if let Some(workspace) = this.workspace.upgrade() {
if this.summary.error_count == 0 && this.summary.warning_count > 0 {
cx.update_default_global(
|show_warnings: &mut IncludeWarnings, _| show_warnings.0 = true,
);
}
workspace.update(cx, |workspace, cx| {
ProjectDiagnosticsEditor::deploy(
workspace,

View File

@@ -99,9 +99,6 @@ pub struct ComposeCompletion {
pub struct ConfirmCodeAction {
#[serde(default)]
pub item_ix: Option<usize>,
#[serde(default)]
#[serde(skip)]
pub from_mouse_context_menu: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
@@ -306,6 +303,8 @@ actions!(
GoToPreviousHunk,
GoToImplementation,
GoToImplementationSplit,
GoToNextChange,
GoToPreviousChange,
GoToPreviousDiagnostic,
GoToTypeDefinition,
GoToTypeDefinitionSplit,

View File

@@ -632,6 +632,7 @@ impl CompletionsMenu {
MarkdownElement::new(markdown.clone(), hover_markdown_style(window, cx))
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
border: false,
})
.on_url_click(open_markdown_url),
)
@@ -774,13 +775,36 @@ pub struct AvailableCodeAction {
pub provider: Rc<dyn CodeActionProvider>,
}
#[derive(Clone, Default)]
#[derive(Clone)]
pub struct CodeActionContents {
pub tasks: Option<Rc<ResolvedTasks>>,
pub actions: Option<Rc<[AvailableCodeAction]>>,
tasks: Option<Rc<ResolvedTasks>>,
actions: Option<Rc<[AvailableCodeAction]>>,
}
impl CodeActionContents {
pub fn new(
mut tasks: Option<ResolvedTasks>,
actions: Option<Rc<[AvailableCodeAction]>>,
cx: &App,
) -> Self {
if !cx.has_flag::<Debugger>() {
if let Some(tasks) = &mut tasks {
tasks
.templates
.retain(|(_, task)| !matches!(task.task_type(), task::TaskType::Debug(_)));
}
}
Self {
tasks: tasks.map(Rc::new),
actions,
}
}
pub fn tasks(&self) -> Option<&ResolvedTasks> {
self.tasks.as_deref()
}
fn len(&self) -> usize {
match (&self.tasks, &self.actions) {
(Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
@@ -790,7 +814,7 @@ impl CodeActionContents {
}
}
pub fn is_empty(&self) -> bool {
fn is_empty(&self) -> bool {
match (&self.tasks, &self.actions) {
(Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
(Some(tasks), None) => tasks.templates.is_empty(),
@@ -799,7 +823,7 @@ impl CodeActionContents {
}
}
pub fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
self.tasks
.iter()
.flat_map(|tasks| {
@@ -867,14 +891,14 @@ pub enum CodeActionsItem {
}
impl CodeActionsItem {
pub fn as_task(&self) -> Option<&ResolvedTask> {
fn as_task(&self) -> Option<&ResolvedTask> {
let Self::Task(_, task) = self else {
return None;
};
Some(task)
}
pub fn as_code_action(&self) -> Option<&CodeAction> {
fn as_code_action(&self) -> Option<&CodeAction> {
let Self::CodeAction { action, .. } = self else {
return None;
};
@@ -988,17 +1012,6 @@ impl CodeActionsMenu {
.iter()
.skip(range.start)
.take(range.end - range.start)
.filter(|action| {
if action
.as_task()
.map(|task| matches!(task.task_type(), task::TaskType::Debug(_)))
.unwrap_or(false)
{
cx.has_flag::<Debugger>()
} else {
true
}
})
.enumerate()
.map(|(ix, action)| {
let item_ix = range.start + ix;
@@ -1014,7 +1027,6 @@ impl CodeActionsMenu {
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
from_mouse_context_menu: false,
},
window,
cx,
@@ -1040,7 +1052,6 @@ impl CodeActionsMenu {
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
from_mouse_context_menu: false,
},
window,
cx,

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