Compare commits

..

37 Commits

Author SHA1 Message Date
Agus Zubiaga
5de2c28f75 Build Zed 2025-08-07 14:59:45 -03:00
Agus Zubiaga
2ecb5b2ff6 Use tempfile tempdir instead of hardcoding /private/tmp 2025-08-07 13:35:21 -03:00
Agus Zubiaga
f1af9d5fbd Use same machine as eval 2025-08-07 12:27:58 -03:00
Agus Zubiaga
d97e15dcaf Raise timeout 2025-08-07 12:24:03 -03:00
Agus Zubiaga
5db22c9440 Install nextest 2025-08-07 12:02:51 -03:00
Agus Zubiaga
49ef4b5024 Run claude e2e tests in CI - attempt 1 2025-08-07 12:00:46 -03:00
Agus Zubiaga
cace7de723 Fix double take 2025-08-07 11:43:45 -03:00
Agus Zubiaga
3925aa9b29 Remove CI workflow for now 2025-08-07 11:37:24 -03:00
Agus Zubiaga
7f9adae3a3 Combine end_turn_tx and cancellation_state into one enum 2025-08-07 11:36:29 -03:00
Agus Zubiaga
4b94e90899 Use replace 2025-08-07 01:21:03 -03:00
Agus Zubiaga
63cc3291e3 Fix CC tool state on cancel 2025-08-07 01:11:13 -03:00
Anthony Eid
f1e69f6311 gpui: Impl Default for ClickEvent (#35751)
While default for ClickEvent shouldn't be used much this is helpful for
other projects using gpui besides Zed. Mainly because the orphan rule
prevents those projects from implementing their own default trait

cc: @huacnlee 

Release Notes:

- N/A
2025-08-06 23:24:37 -04:00
Agus Zubiaga
bd1c26cb5b Fix interrupting ACP threads and CC cancellation (#35752)
Fixes a bug where generation wouldn't continue after interrupting the
agent, and improves CC cancellation so we don't display "[Request
interrupted by user]"

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-08-06 22:55:17 -03:00
Richard Feldman
1907b16fe6 Establish WebSocket connection to Cloud (#35734)
This PR adds a new WebSocket connection to Cloud.

This connection will be used to push down notifications from the server
to the client.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-08-07 01:28:41 +00:00
Max Brunsfeld
c595a7576d Fix git hunk staging on windows (#35755)
We were failing to flush the Git process's `stdin` before dropping it.

Release Notes:

- N/A
2025-08-06 17:30:36 -07:00
Danilo Leal
8e290b446e thread view: Add UI refinements (#35754)
More notably around how we render tool calls. Nothing too drastic,
though.

Release Notes:

- N/A
2025-08-06 20:31:11 -03:00
Marshall Bowers
58392b9c13 cloud_api_types: Add types for WebSocket protocol (#35753)
This PR adds types for the Cloud WebSocket protocol to the
`cloud_api_types` crate.

Release Notes:

- N/A
2025-08-06 23:20:04 +00:00
Cole Miller
9358690337 Fix flicker when agent plan updates (#35739)
Currently, when the agent updates its plan, there are a few frames where
the text after `Current:` in the plan summary is blank, causing a
flicker. This is because we treat that field as markdown, and the
`MarkdownElement` renders as blank until the raw text has finished
parsing in the background.

This PR fixes the flicker by changing `Markdown::new_text` to
optimistically render the source as a single `MarkdownEvent::Text` span
until background parsing has finished.

Release Notes:

- N/A

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-08-06 22:38:00 +00:00
Anthony Eid
3ea90e397b debugger: Filter out debug scenarios with invalid Adapters from debug picker (#35744)
I also removed a debug assertion that wasn't true when a debug session
was restarting through a request, because there wasn't a booting task
Zed needed to run before the session.

I renamed SessionState::Building to SessionState::Booting as well,
because building implies that we're building code while booting the
session covers more cases and is more accurate.

Release Notes:

- debugger: Filter out more invalid debug configurations from the debug
picker

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-08-06 18:10:17 -04:00
Peter Tripp
a5dd8d0052 Recognize pixi.lock as YAML (#35747)
Release Notes:

- N/A
2025-08-06 15:56:11 -04:00
Agus Zubiaga
250c51bb20 Fix syntax highlighting in ACP diffs (#35748)
Release Notes:

- N/A
2025-08-06 19:53:45 +00:00
Anthony Eid
010441e23b debugger: Show run to cursor in editor's context menu (#35745)
This also fixed a bug where evaluate selected text was an available
option when the selected debug session was terminated.


Release Notes:

- debugger: add Run to Cursor back to Editor's context menu

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-08-06 15:45:22 -04:00
Peter Tripp
f9038f6189 Add key contexts for Pickers (#35665)
Closes: https://github.com/zed-industries/zed/issues/35430

Added:
- Workspace > CommandPalette
- Workspace > GitBranchSelector
- Workspace > GitRepositorySelector
- Workspace > RecentProjects
- Workspace > LanguageSelector
- Workspace > IconThemeSelector
- Workspace > ThemeSelector

Release Notes:

- Added new keymap contexts for various Pickers - CommandPalette,
GitBranchSelector, GitRepositorySelector, RecentProjects,
LanguageSelector, IconThemeSelector, ThemeSelector
2025-08-06 15:28:18 -04:00
xdBronch
a80da784b7 lsp: Advertise support for markdown in completion documentation (#35727)
Release Notes:

- N/A
2025-08-06 21:42:29 +03:00
Piotr Osiewicz
fb1f9d1212 lsp: Correctly serialize errors for LSP requests + improve handling of unrecognized methods (#35738)
We used to not respond at all to requests that we didn't have a handler
for, which is yuck. It may have left the language server waiting for the
response for no good reason. The other (worse) finding is that we did
not have a full definition of an Error type for LSP, which made it so
that a spec-compliant language server would fail to deserialize our
response (with an error). This then could lead to all sorts of
funkiness, including hangs and crashes on the language server's part.

Co-authored-by: Lukas <lukas@zed.dev>
Co-authored-by: Remco Smits <djsmits12@gmail.com>

Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Closes #ISSUE

Release Notes:

- Improved reporting of errors to language servers, which should improve
the stability of LSPs ran by Zed.

---------

Co-authored-by: Lukas <lukas@zed.dev>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-08-06 18:27:48 +00:00
Mikayla Maki
794098e5c9 Update instructions for local collaboration (#35689)
Release Notes:

- N/A
2025-08-06 11:10:28 -07:00
Marshall Bowers
b08e26df60 collab: Remove unused StripeBilling methods (#35740)
This PR removes some unused methods from the `StripeBilling` object.

Release Notes:

- N/A
2025-08-06 17:42:12 +00:00
Marshall Bowers
740597492b collab: Remove Stripe events polling (#35736)
This PR removes the Stripe event polling from Collab, as it has been
moved to Cloud.

Release Notes:

- N/A
2025-08-06 16:53:43 +00:00
Ben Kunkle
ebda6b8a94 keymap_ui: Show matching bindings (#35732)
Closes #ISSUE

Adds a bit of text in the keybind editing modal when there are existing
keystrokes with the same key, with the ability for the user to click the
text and have the keymap editor search be updated to show only bindings
with those keystrokes

Release Notes:

- Keymap Editor: Added a warning to the keybind editing modal when
existing bindings have the same keystrokes. Clicking the warning will
close the modal and show bindings with the entered keystrokes in the
keymap editor. This behavior was previously possible with the
`keymap_editor::ShowMatchingKeybinds` action in the Keymap Editor, and
is now present in the keybind editing modal as well.
2025-08-06 12:16:05 -04:00
Kirill Bulatov
55b4df4d9f Add a way to distinguish metrics by Zed's release channel (#35729)
Release Notes:

- N/A

---------

Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
2025-08-06 18:47:44 +03:00
Umesh Yadav
b8e8fbd8e6 ollama: Add support for gpt-oss (#35648)
There is a know bug when calling tool discussion:
https://discord.com/channels/1128867683291627614/1402385744038858853
I have raised the issue with ollama team and they are currently fixing
it.

Release Notes:

- ollama: Add support for gpt-oss
2025-08-06 10:44:15 -04:00
Agus Zubiaga
33f198fef1 Thread view scrollbar (#35655)
This also adds a convenient `Scrollbar:auto_hide` function so that we
don't have to handle that at the callsite.

Release Notes:

- N/A

---------

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-08-06 14:01:34 +00:00
Peter Tripp
3c602fecbf docs: Cleanup tool use documentation (#35725)
Remove redundant documentation about tool use.

Release Notes:

- N/A
2025-08-06 09:59:13 -04:00
Agus Zubiaga
334bdd0efc Fix acp thread entry width (#35723)
Release Notes:

- N/A
2025-08-06 13:39:55 +00:00
Agus Zubiaga
69dc870828 Fix CC todo tool parsing (#35721)
It looks like the TODO tool call no longer requires a priority.

Release Notes:

- N/A
2025-08-06 13:27:11 +00:00
Agus Zubiaga
22fa41e9c0 Handle CC thinking (#35722)
Release Notes:

- N/A
2025-08-06 13:20:53 +00:00
Joseph T. Lyons
7e790f52c8 Bump Zed to v0.200 (#35719)
🎉

Release Notes:

-N/A
2025-08-06 13:11:46 +00:00
101 changed files with 1486 additions and 2024 deletions

View File

@@ -5,28 +5,25 @@ self-hosted-runner:
# GitHub-hosted Runners
- github-8vcpu-ubuntu-2404
- github-16vcpu-ubuntu-2404
- github-32vcpu-ubuntu-2404
- github-8vcpu-ubuntu-2204
- github-16vcpu-ubuntu-2204
- github-32vcpu-ubuntu-2204
- github-16vcpu-ubuntu-2204-arm
- windows-2025-16
- windows-2025-32
- windows-2025-64
# Namespace Ubuntu 20.04 (Release builds)
- namespace-profile-16x32-ubuntu-2004
- namespace-profile-32x64-ubuntu-2004
- namespace-profile-16x32-ubuntu-2004-arm
- namespace-profile-32x64-ubuntu-2004-arm
# Namespace Ubuntu 22.04 (Everything else)
- namespace-profile-2x4-ubuntu-2204
- namespace-profile-4x8-ubuntu-2204
- namespace-profile-8x16-ubuntu-2204
- namespace-profile-16x32-ubuntu-2204
- namespace-profile-32x64-ubuntu-2204
# Namespace Limited Preview
- namespace-profile-8x16-ubuntu-2004-arm-m4
- namespace-profile-8x32-ubuntu-2004-arm-m4
# Buildjet Ubuntu 20.04 - AMD x86_64
- buildjet-2vcpu-ubuntu-2004
- buildjet-4vcpu-ubuntu-2004
- buildjet-8vcpu-ubuntu-2004
- buildjet-16vcpu-ubuntu-2004
- buildjet-32vcpu-ubuntu-2004
# Buildjet Ubuntu 22.04 - AMD x86_64
- buildjet-2vcpu-ubuntu-2204
- buildjet-4vcpu-ubuntu-2204
- buildjet-8vcpu-ubuntu-2204
- buildjet-16vcpu-ubuntu-2204
- buildjet-32vcpu-ubuntu-2204
# Buildjet Ubuntu 22.04 - Graviton aarch64
- buildjet-8vcpu-ubuntu-2204-arm
- buildjet-16vcpu-ubuntu-2204-arm
- buildjet-32vcpu-ubuntu-2204-arm
# Self Hosted Runners
- self-mini-macos
- self-32vcpu-windows-2022

View File

@@ -13,7 +13,7 @@ runs:
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# cache-provider: "buildjet"
cache-provider: "buildjet"
- name: Install Linux dependencies
shell: bash -euxo pipefail {0}

132
.github/workflows/agent_servers_e2e.yml vendored Normal file
View File

@@ -0,0 +1,132 @@
name: Agent Servers E2E Tests
on:
schedule:
- cron: "0 12 * * *"
push:
branches:
- as-e2e-ci
pull_request:
branches:
- "**"
paths:
- "crates/agent_servers/**"
- "crates/acp_thread/**"
- ".github/workflows/agent_servers_e2e.yml"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
jobs:
e2e-tests:
name: Run Agent Servers E2E Tests
if: github.repository_owner == 'zed-industries'
timeout-minutes: 20
runs-on:
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
# - name: Checkout gemini-cli repo
# uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# with:
# repository: zed-industries/gemini-cli
# ref: migrate-acp
# path: gemini-cli
# clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
- name: Install Linux dependencies
run: ./script/linux
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "18"
- name: Install Claude Code CLI
shell: bash -euxo pipefail {0}
run: |
npm install -g @anthropic-ai/claude-code
# Verify installation
which claude || echo "Claude CLI not found in PATH"
# Skip authentication if API key is not set (tests may use mock)
if [ -n "$ANTHROPIC_API_KEY" ]; then
echo "Anthropic API key is configured"
fi
# - name: Install and setup Gemini CLI
# shell: bash -euxo pipefail {0}
# run: |
# # Also install dependencies for local gemini-cli repo
# pushd gemini-cli
# npm install
# npm run build
# popd
# # Verify installations
# which gemini || echo "Gemini CLI not found in PATH"
# # Skip authentication if API key is not set (tests may use mock)
# if [ -n "$GEMINI_API_KEY" ]; then
# echo "Gemini API key is configured"
# fi
- name: Limit target directory size
shell: bash -euxo pipefail {0}
run: script/clear-target-dir-if-larger-than 100
- name: Install nextest
shell: bash -euxo pipefail {0}
run: |
cargo install cargo-nextest --locked
- name: Build Zed
shell: bash -euxo pipefail {0}
run: |
cargo build
- name: Run E2E tests
shell: bash -euxo pipefail {0}
run: |
cargo nextest run \
--package agent_servers \
--features e2e \
--no-fail-fast \
claude
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
# to clean up the config file, Ive included the cleanup code here as a precaution.
# While its not strictly necessary at this moment, I believe its better to err on the side of caution.
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo

View File

@@ -16,7 +16,7 @@ jobs:
bump_patch_version:
if: github.repository_owner == 'zed-industries'
runs-on:
- namespace-profile-16x32-ubuntu-2204
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

View File

@@ -137,7 +137,7 @@ jobs:
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- namespace-profile-8x16-ubuntu-2204
- buildjet-8vcpu-ubuntu-2204
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -168,7 +168,7 @@ jobs:
needs: [job_spec]
if: github.repository_owner == 'zed-industries'
runs-on:
- namespace-profile-4x8-ubuntu-2204
- buildjet-8vcpu-ubuntu-2204
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -221,7 +221,7 @@ jobs:
github.repository_owner == 'zed-industries' &&
(needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true')
runs-on:
- namespace-profile-8x16-ubuntu-2204
- buildjet-8vcpu-ubuntu-2204
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -328,7 +328,7 @@ jobs:
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- namespace-profile-16x32-ubuntu-2204
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
@@ -342,7 +342,7 @@ jobs:
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# cache-provider: "buildjet"
cache-provider: "buildjet"
- name: Install Linux dependencies
run: ./script/linux
@@ -380,7 +380,7 @@ jobs:
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- namespace-profile-16x32-ubuntu-2204
- buildjet-8vcpu-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
@@ -394,7 +394,7 @@ jobs:
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# cache-provider: "buildjet"
cache-provider: "buildjet"
- name: Install Clang & Mold
run: ./script/remote-server && ./script/install-mold 2.34.0
@@ -511,8 +511,8 @@ jobs:
runs-on:
- self-mini-macos
if: |
( startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling') )
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [macos_tests]
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
@@ -597,10 +597,10 @@ jobs:
timeout-minutes: 60
name: Linux x86_x64 release bundle
runs-on:
- namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc
- buildjet-16vcpu-ubuntu-2004 # ubuntu 20.04 for minimal glibc
if: |
( startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling') )
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
needs: [linux_tests]
steps:
- name: Checkout repo
@@ -650,7 +650,7 @@ jobs:
timeout-minutes: 60
name: Linux arm64 release bundle
runs-on:
- namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc
- buildjet-32vcpu-ubuntu-2204-arm
if: |
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
@@ -703,8 +703,10 @@ jobs:
timeout-minutes: 60
runs-on: github-8vcpu-ubuntu-2404
if: |
false && ( startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling') )
false && (
startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
)
needs: [linux_tests]
name: Build Zed on FreeBSD
steps:

View File

@@ -9,7 +9,7 @@ jobs:
deploy-docs:
name: Deploy Docs
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-16x32-ubuntu-2204
runs-on: buildjet-16vcpu-ubuntu-2204
steps:
- name: Checkout repo

View File

@@ -61,7 +61,7 @@ jobs:
- style
- tests
runs-on:
- namespace-profile-16x32-ubuntu-2204
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Install doctl
uses: digitalocean/action-doctl@v2
@@ -94,7 +94,7 @@ jobs:
needs:
- publish
runs-on:
- namespace-profile-16x32-ubuntu-2204
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Checkout repo

View File

@@ -32,7 +32,7 @@ jobs:
github.repository_owner == 'zed-industries' &&
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
runs-on:
- namespace-profile-16x32-ubuntu-2204
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
@@ -46,7 +46,7 @@ jobs:
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# cache-provider: "buildjet"
cache-provider: "buildjet"
- name: Install Linux dependencies
run: ./script/linux

View File

@@ -20,7 +20,7 @@ jobs:
matrix:
system:
- os: x86 Linux
runner: namespace-profile-16x32-ubuntu-2204
runner: buildjet-16vcpu-ubuntu-2204
install_nix: true
- os: arm Mac
runner: [macOS, ARM64, test]

View File

@@ -20,7 +20,7 @@ jobs:
name: Run randomized tests
if: github.repository_owner == 'zed-industries'
runs-on:
- namespace-profile-16x32-ubuntu-2204
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

View File

@@ -128,7 +128,7 @@ jobs:
name: Create a Linux *.tar.gz bundle for x86
if: github.repository_owner == 'zed-industries'
runs-on:
- namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc
- buildjet-16vcpu-ubuntu-2004
needs: tests
steps:
- name: Checkout repo
@@ -168,7 +168,7 @@ jobs:
name: Create a Linux *.tar.gz bundle for ARM
if: github.repository_owner == 'zed-industries'
runs-on:
- namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc
- buildjet-32vcpu-ubuntu-2204-arm
needs: tests
steps:
- name: Checkout repo

View File

@@ -23,7 +23,7 @@ jobs:
timeout-minutes: 60
name: Run unit evals
runs-on:
- namespace-profile-16x32-ubuntu-2204
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
@@ -37,7 +37,7 @@ jobs:
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# cache-provider: "buildjet"
cache-provider: "buildjet"
- name: Install Linux dependencies
run: ./script/linux

10
Cargo.lock generated
View File

@@ -9123,7 +9123,6 @@ dependencies = [
"anyhow",
"base64 0.22.1",
"client",
"cloud_api_types",
"cloud_llm_client",
"collections",
"futures 0.3.31",
@@ -11184,7 +11183,6 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
"log",
"schemars",
"serde",
"serde_json",
@@ -16620,8 +16618,9 @@ dependencies = [
[[package]]
name = "tiktoken-rs"
version = "0.8.0"
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=30c32a4522751699adeda0d5840c71c3b75ae73d#30c32a4522751699adeda0d5840c71c3b75ae73d"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25563eeba904d770acf527e8b370fe9a5547bacd20ff84a0b6c3bc41288e5625"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -20461,7 +20460,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.199.9"
version = "0.200.0"
dependencies = [
"activity_indicator",
"agent",
@@ -20864,7 +20863,6 @@ dependencies = [
"menu",
"postage",
"project",
"rand 0.8.5",
"regex",
"release_channel",
"reqwest_client",

View File

@@ -601,7 +601,7 @@ sysinfo = "0.31.0"
take-until = "0.2.0"
tempfile = "3.20.0"
thiserror = "2.0.12"
tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "30c32a4522751699adeda0d5840c71c3b75ae73d" }
tiktoken-rs = "0.7.0"
time = { version = "0.3", features = [
"macros",
"parsing",

View File

@@ -1,3 +1,4 @@
collab: RUST_LOG=${RUST_LOG:-info} cargo run --package=collab serve all
cloud: cd ../cloud; cargo make dev
livekit: livekit-server --dev
blob_store: ./script/run-local-minio

View File

Before

Width:  |  Height:  |  Size: 776 B

After

Width:  |  Height:  |  Size: 776 B

View File

@@ -332,7 +332,9 @@
"enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage",
"shift-ctrl-r": "agent::OpenAgentDiff"
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
{

View File

@@ -384,7 +384,9 @@
"enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage",
"shift-ctrl-r": "agent::OpenAgentDiff"
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll"
}
},
{

View File

@@ -6,6 +6,7 @@ use anyhow::{Context as _, Result};
use assistant_tool::ActionLog;
use buffer_diff::BufferDiff;
use editor::{Bias, MultiBuffer, PathKey};
use futures::future::{Fuse, FusedFuture};
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task};
use itertools::Itertools;
@@ -221,7 +222,9 @@ impl ToolCall {
}
if let Some(title) = title {
self.label = cx.new(|cx| Markdown::new_text(title.into(), cx));
self.label.update(cx, |label, cx| {
label.replace(title, cx);
});
}
if let Some(content) = content {
@@ -411,8 +414,6 @@ impl ToolCallContent {
pub struct Diff {
pub multibuffer: Entity<MultiBuffer>,
pub path: PathBuf,
pub new_buffer: Entity<Buffer>,
pub old_buffer: Entity<Buffer>,
_task: Task<Result<()>>,
}
@@ -433,23 +434,34 @@ impl Diff {
let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx));
let new_buffer_snapshot = new_buffer.read(cx).text_snapshot();
let old_buffer_snapshot = old_buffer.read(cx).snapshot();
let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx));
let diff_task = buffer_diff.update(cx, |diff, cx| {
diff.set_base_text(
old_buffer_snapshot,
Some(language_registry.clone()),
new_buffer_snapshot,
cx,
)
});
let task = cx.spawn({
let multibuffer = multibuffer.clone();
let path = path.clone();
let new_buffer = new_buffer.clone();
async move |cx| {
diff_task.await?;
let language = language_registry
.language_for_file_path(&path)
.await
.log_err();
new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?;
let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| {
buffer.set_language(language, cx);
buffer.snapshot()
})?;
buffer_diff
.update(cx, |diff, cx| {
diff.set_base_text(
old_buffer_snapshot,
Some(language_registry),
new_buffer_snapshot,
cx,
)
})?
.await?;
multibuffer
.update(cx, |multibuffer, cx| {
@@ -468,18 +480,10 @@ impl Diff {
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
multibuffer.add_diff(buffer_diff.clone(), cx);
multibuffer.add_diff(buffer_diff, cx);
})
.log_err();
if let Some(language) = language_registry
.language_for_file_path(&path)
.await
.log_err()
{
new_buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx))?;
}
anyhow::Ok(())
}
});
@@ -487,8 +491,6 @@ impl Diff {
Self {
multibuffer,
path,
new_buffer,
old_buffer,
_task: task,
}
}
@@ -557,7 +559,7 @@ pub struct PlanEntry {
impl PlanEntry {
pub fn from_acp(entry: acp::PlanEntry, cx: &mut App) -> Self {
Self {
content: cx.new(|cx| Markdown::new_text(entry.content.into(), cx)),
content: cx.new(|cx| Markdown::new(entry.content.into(), None, None, cx)),
priority: entry.priority,
status: entry.status,
}
@@ -571,7 +573,7 @@ pub struct AcpThread {
project: Entity<Project>,
action_log: Entity<ActionLog>,
shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
send_task: Option<Task<()>>,
send_task: Option<Fuse<Task<()>>>,
connection: Rc<dyn AgentConnection>,
session_id: acp::SessionId,
}
@@ -661,7 +663,11 @@ impl AcpThread {
}
pub fn status(&self) -> ThreadStatus {
if self.send_task.is_some() {
if self
.send_task
.as_ref()
.map_or(false, |t| !t.is_terminated())
{
if self.waiting_for_tool_confirmation() {
ThreadStatus::WaitingForToolConfirmation
} else {
@@ -971,13 +977,26 @@ impl AcpThread {
}
pub fn update_plan(&mut self, request: acp::Plan, cx: &mut Context<Self>) {
self.plan = Plan {
entries: request
.entries
.into_iter()
.map(|entry| PlanEntry::from_acp(entry, cx))
.collect(),
};
let new_entries_len = request.entries.len();
let mut new_entries = request.entries.into_iter();
// Reuse existing markdown to prevent flickering
for (old, new) in self.plan.entries.iter_mut().zip(new_entries.by_ref()) {
let PlanEntry {
content,
priority,
status,
} = old;
content.update(cx, |old, cx| {
old.replace(new.content, cx);
});
*priority = new.priority;
*status = new.status;
}
for new in new_entries {
self.plan.entries.push(PlanEntry::from_acp(new, cx))
}
self.plan.entries.truncate(new_entries_len);
cx.notify();
}
@@ -1023,28 +1042,31 @@ impl AcpThread {
let (tx, rx) = oneshot::channel();
let cancel_task = self.cancel(cx);
self.send_task = Some(cx.spawn(async move |this, cx| {
async {
cancel_task.await;
self.send_task = Some(
cx.spawn(async move |this, cx| {
async {
cancel_task.await;
let result = this
.update(cx, |this, cx| {
this.connection.prompt(
acp::PromptRequest {
prompt: message,
session_id: this.session_id.clone(),
},
cx,
)
})?
.await;
tx.send(result).log_err();
this.update(cx, |this, _cx| this.send_task.take())?;
anyhow::Ok(())
}
.await
.log_err();
}));
let result = this
.update(cx, |this, cx| {
this.connection.prompt(
acp::PromptRequest {
prompt: message,
session_id: this.session_id.clone(),
},
cx,
)
})?
.await;
tx.send(result).log_err();
anyhow::Ok(())
}
.await
.log_err();
})
.fuse(),
);
cx.spawn(async move |this, cx| match rx.await {
Ok(Err(e)) => {

View File

@@ -24,7 +24,7 @@ use futures::{
};
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
use serde::{Deserialize, Serialize};
use util::ResultExt;
use util::{ResultExt, debug_panic};
use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
use crate::claude::tools::ClaudeTool;
@@ -153,16 +153,17 @@ impl AgentConnection for ClaudeAgentConnection {
})
.detach();
let end_turn_tx = Rc::new(RefCell::new(None));
let turn_state = Rc::new(RefCell::new(TurnState::None));
let handler_task = cx.spawn({
let end_turn_tx = end_turn_tx.clone();
let turn_state = turn_state.clone();
let mut thread_rx = thread_rx.clone();
async move |cx| {
while let Some(message) = incoming_message_rx.next().await {
ClaudeAgentSession::handle_message(
thread_rx.clone(),
message,
end_turn_tx.clone(),
turn_state.clone(),
cx,
)
.await
@@ -188,7 +189,7 @@ impl AgentConnection for ClaudeAgentConnection {
let session = ClaudeAgentSession {
outgoing_tx,
end_turn_tx,
turn_state,
_handler_task: handler_task,
_mcp_server: Some(permission_mcp_server),
};
@@ -220,8 +221,8 @@ impl AgentConnection for ClaudeAgentConnection {
)));
};
let (tx, rx) = oneshot::channel();
session.end_turn_tx.borrow_mut().replace(tx);
let (end_tx, end_rx) = oneshot::channel();
session.turn_state.replace(TurnState::InProgress { end_tx });
let mut content = String::new();
for chunk in params.prompt {
@@ -255,7 +256,7 @@ impl AgentConnection for ClaudeAgentConnection {
return Task::ready(Err(anyhow!(err)));
}
cx.foreground_executor().spawn(async move { rx.await? })
cx.foreground_executor().spawn(async move { end_rx.await? })
}
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
@@ -265,18 +266,27 @@ impl AgentConnection for ClaudeAgentConnection {
return;
};
let request_id = new_request_id();
let turn_state = session.turn_state.take();
let TurnState::InProgress { end_tx } = turn_state else {
// Already cancelled or idle, put it back
session.turn_state.replace(turn_state);
return;
};
session.turn_state.replace(TurnState::CancelRequested {
end_tx,
request_id: request_id.clone(),
});
session
.outgoing_tx
.unbounded_send(SdkMessage::new_interrupt_message())
.unbounded_send(SdkMessage::ControlRequest {
request_id,
request: ControlRequest::Interrupt,
})
.log_err();
if let Some(end_turn_tx) = session.end_turn_tx.borrow_mut().take() {
end_turn_tx
.send(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
}))
.ok();
}
}
}
@@ -338,26 +348,139 @@ fn spawn_claude(
struct ClaudeAgentSession {
outgoing_tx: UnboundedSender<SdkMessage>,
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<acp::PromptResponse>>>>>,
turn_state: Rc<RefCell<TurnState>>,
_mcp_server: Option<ClaudeZedMcpServer>,
_handler_task: Task<()>,
}
#[derive(Debug, Default)]
enum TurnState {
#[default]
None,
InProgress {
end_tx: oneshot::Sender<Result<acp::PromptResponse>>,
},
CancelRequested {
end_tx: oneshot::Sender<Result<acp::PromptResponse>>,
request_id: String,
},
CancelConfirmed {
end_tx: oneshot::Sender<Result<acp::PromptResponse>>,
},
}
impl TurnState {
fn is_cancelled(&self) -> bool {
matches!(self, TurnState::CancelConfirmed { .. })
}
fn end_tx(self) -> Option<oneshot::Sender<Result<acp::PromptResponse>>> {
match self {
TurnState::None => None,
TurnState::InProgress { end_tx, .. } => Some(end_tx),
TurnState::CancelRequested { end_tx, .. } => Some(end_tx),
TurnState::CancelConfirmed { end_tx } => Some(end_tx),
}
}
fn confirm_cancellation(self, id: &str) -> Self {
match self {
TurnState::CancelRequested { request_id, end_tx } if request_id == id => {
TurnState::CancelConfirmed { end_tx }
}
_ => self,
}
}
}
impl ClaudeAgentSession {
async fn handle_message(
mut thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
message: SdkMessage,
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<acp::PromptResponse>>>>>,
turn_state: Rc<RefCell<TurnState>>,
cx: &mut AsyncApp,
) {
match message {
// we should only be sending these out, they don't need to be in the thread
SdkMessage::ControlRequest { .. } => {}
SdkMessage::Assistant {
SdkMessage::User {
message,
session_id: _,
} => {
let Some(thread) = thread_rx
.recv()
.await
.log_err()
.and_then(|entity| entity.upgrade())
else {
log::error!("Received an SDK message but thread is gone");
return;
};
for chunk in message.content.chunks() {
match chunk {
ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
if !turn_state.borrow().is_cancelled() {
thread
.update(cx, |thread, cx| {
thread.push_user_content_block(text.into(), cx)
})
.log_err();
}
}
ContentChunk::ToolResult {
content,
tool_use_id,
} => {
let content = content.to_string();
thread
.update(cx, |thread, cx| {
thread.update_tool_call(
acp::ToolCallUpdate {
id: acp::ToolCallId(tool_use_id.into()),
fields: acp::ToolCallUpdateFields {
status: if turn_state.borrow().is_cancelled() {
// Do not set to completed if turn was cancelled
None
} else {
Some(acp::ToolCallStatus::Completed)
},
content: (!content.is_empty())
.then(|| vec![content.into()]),
..Default::default()
},
},
cx,
)
})
.log_err();
}
ContentChunk::Thinking { .. }
| ContentChunk::RedactedThinking
| ContentChunk::ToolUse { .. } => {
debug_panic!(
"Should not get {:?} with role: assistant. should we handle this?",
chunk
);
}
ContentChunk::Image
| ContentChunk::Document
| ContentChunk::WebSearchToolResult => {
thread
.update(cx, |thread, cx| {
thread.push_assistant_content_block(
format!("Unsupported content: {:?}", chunk).into(),
false,
cx,
)
})
.log_err();
}
}
}
}
| SdkMessage::User {
SdkMessage::Assistant {
message,
session_id: _,
} => {
@@ -380,6 +503,24 @@ impl ClaudeAgentSession {
})
.log_err();
}
ContentChunk::Thinking { thinking } => {
thread
.update(cx, |thread, cx| {
thread.push_assistant_content_block(thinking.into(), true, cx)
})
.log_err();
}
ContentChunk::RedactedThinking => {
thread
.update(cx, |thread, cx| {
thread.push_assistant_content_block(
"[REDACTED]".into(),
true,
cx,
)
})
.log_err();
}
ContentChunk::ToolUse { id, name, input } => {
let claude_tool = ClaudeTool::infer(&name, input);
@@ -405,33 +546,12 @@ impl ClaudeAgentSession {
})
.log_err();
}
ContentChunk::ToolResult {
content,
tool_use_id,
} => {
let content = content.to_string();
thread
.update(cx, |thread, cx| {
thread.update_tool_call(
acp::ToolCallUpdate {
id: acp::ToolCallId(tool_use_id.into()),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
content: (!content.is_empty())
.then(|| vec![content.into()]),
..Default::default()
},
},
cx,
)
})
.log_err();
ContentChunk::ToolResult { .. } | ContentChunk::WebSearchToolResult => {
debug_panic!(
"Should not get tool results with role: assistant. should we handle this?"
);
}
ContentChunk::Image
| ContentChunk::Document
| ContentChunk::Thinking
| ContentChunk::RedactedThinking
| ContentChunk::WebSearchToolResult => {
ContentChunk::Image | ContentChunk::Document => {
thread
.update(cx, |thread, cx| {
thread.push_assistant_content_block(
@@ -451,27 +571,41 @@ impl ClaudeAgentSession {
result,
..
} => {
if let Some(end_turn_tx) = end_turn_tx.borrow_mut().take() {
if is_error || subtype == ResultErrorType::ErrorDuringExecution {
end_turn_tx
.send(Err(anyhow!(
"Error: {}",
result.unwrap_or_else(|| subtype.to_string())
)))
.ok();
} else {
let stop_reason = match subtype {
ResultErrorType::Success => acp::StopReason::EndTurn,
ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests,
ResultErrorType::ErrorDuringExecution => unreachable!(),
};
end_turn_tx
.send(Ok(acp::PromptResponse { stop_reason }))
.ok();
}
let turn_state = turn_state.take();
let was_cancelled = turn_state.is_cancelled();
let Some(end_turn_tx) = turn_state.end_tx() else {
debug_panic!("Received `SdkMessage::Result` but there wasn't an active turn");
return;
};
if is_error || (!was_cancelled && subtype == ResultErrorType::ErrorDuringExecution)
{
end_turn_tx
.send(Err(anyhow!(
"Error: {}",
result.unwrap_or_else(|| subtype.to_string())
)))
.ok();
} else {
let stop_reason = match subtype {
ResultErrorType::Success => acp::StopReason::EndTurn,
ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests,
ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled,
};
end_turn_tx
.send(Ok(acp::PromptResponse { stop_reason }))
.ok();
}
}
SdkMessage::System { .. } | SdkMessage::ControlResponse { .. } => {}
SdkMessage::ControlResponse { response } => {
if matches!(response.subtype, ResultErrorType::Success) {
let new_state = turn_state.take().confirm_cancellation(&response.request_id);
turn_state.replace(new_state);
} else {
log::error!("Control response error: {:?}", response);
}
}
SdkMessage::System { .. } => {}
}
}
@@ -580,11 +714,13 @@ enum ContentChunk {
content: Content,
tool_use_id: String,
},
Thinking {
thinking: String,
},
RedactedThinking,
// TODO
Image,
Document,
Thinking,
RedactedThinking,
WebSearchToolResult,
#[serde(untagged)]
UntaggedText(String),
@@ -594,12 +730,12 @@ impl Display for ContentChunk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ContentChunk::Text { text } => write!(f, "{}", text),
ContentChunk::Thinking { thinking } => write!(f, "Thinking: {}", thinking),
ContentChunk::RedactedThinking => write!(f, "Thinking: [REDACTED]"),
ContentChunk::UntaggedText(text) => write!(f, "{}", text),
ContentChunk::ToolResult { content, .. } => write!(f, "{}", content),
ContentChunk::Image
| ContentChunk::Document
| ContentChunk::Thinking
| ContentChunk::RedactedThinking
| ContentChunk::ToolUse { .. }
| ContentChunk::WebSearchToolResult => {
write!(f, "\n{:?}\n", &self)
@@ -710,22 +846,15 @@ impl Display for ResultErrorType {
}
}
impl SdkMessage {
fn new_interrupt_message() -> Self {
use rand::Rng;
// In the Claude Code TS SDK they just generate a random 12 character string,
// `Math.random().toString(36).substring(2, 15)`
let request_id = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(12)
.map(char::from)
.collect();
Self::ControlRequest {
request_id,
request: ControlRequest::Interrupt,
}
}
fn new_request_id() -> String {
use rand::Rng;
// In the Claude Code TS SDK they just generate a random 12 character string,
// `Math.random().toString(36).substring(2, 15)`
rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(12)
.map(char::from)
.collect()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -746,6 +875,8 @@ enum PermissionMode {
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::e2e_tests;
use gpui::TestAppContext;
use serde_json::json;
crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow");
@@ -758,6 +889,71 @@ pub(crate) mod tests {
}
}
#[gpui::test]
#[cfg_attr(not(feature = "e2e"), ignore)]
async fn test_todo_plan(cx: &mut TestAppContext) {
let fs = e2e_tests::init_test(cx).await;
let tempdir = tempfile::tempdir().unwrap();
let project = Project::test(fs, [], cx).await;
let thread =
e2e_tests::new_test_thread(ClaudeCode, project.clone(), tempdir.path(), cx).await;
thread
.update(cx, |thread, cx| {
thread.send_raw(
"Create a todo plan for initializing a new React app. I'll follow it myself, do not execute on it.",
cx,
)
})
.await
.unwrap();
let mut entries_len = 0;
thread.read_with(cx, |thread, _| {
entries_len = thread.plan().entries.len();
assert!(thread.plan().entries.len() > 0, "Empty plan");
});
thread
.update(cx, |thread, cx| {
thread.send_raw(
"Mark the first entry status as in progress without acting on it.",
cx,
)
})
.await
.unwrap();
thread.read_with(cx, |thread, _| {
assert!(matches!(
thread.plan().entries[0].status,
acp::PlanEntryStatus::InProgress
));
assert_eq!(thread.plan().entries.len(), entries_len);
});
thread
.update(cx, |thread, cx| {
thread.send_raw(
"Now mark the first entry as completed without acting on it.",
cx,
)
})
.await
.unwrap();
thread.read_with(cx, |thread, _| {
assert!(matches!(
thread.plan().entries[0].status,
acp::PlanEntryStatus::Completed
));
assert_eq!(thread.plan().entries.len(), entries_len);
});
drop(tempdir);
}
#[test]
fn test_deserialize_content_untagged_text() {
let json = json!("Hello, world!");

View File

@@ -143,25 +143,6 @@ impl ClaudeTool {
Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
Self::WebSearch(Some(params)) => vec![params.to_string().into()],
Self::TodoWrite(Some(params)) => vec![
params
.todos
.iter()
.map(|todo| {
format!(
"- {} {}: {}",
match todo.status {
TodoStatus::Completed => "",
TodoStatus::InProgress => "🚧",
TodoStatus::Pending => "",
},
todo.priority,
todo.content
)
})
.join("\n")
.into(),
],
Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
@@ -193,6 +174,10 @@ impl ClaudeTool {
})
.unwrap_or_default()
}
Self::TodoWrite(Some(_)) => {
// These are mapped to plan updates later
vec![]
}
Self::Task(None)
| Self::NotebookRead(None)
| Self::NotebookEdit(None)
@@ -488,10 +473,11 @@ impl std::fmt::Display for GrepToolParams {
}
}
#[derive(Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
#[derive(Default, Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TodoPriority {
High,
#[default]
Medium,
Low,
}
@@ -526,14 +512,13 @@ impl Into<acp::PlanEntryStatus> for TodoStatus {
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
pub struct Todo {
/// Unique identifier
pub id: String,
/// Task description
pub content: String,
/// Priority level of the todo
pub priority: TodoPriority,
/// Current status of the todo
pub status: TodoStatus,
/// Priority level of the todo
#[serde(default)]
pub priority: TodoPriority,
}
impl Into<acp::PlanEntry> for Todo {

View File

@@ -13,12 +13,12 @@ use gpui::{Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use settings::{Settings, SettingsStore};
use util::path;
pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
let fs = init_test(cx).await;
let tempdir = tempfile::tempdir().unwrap();
let project = Project::test(fs, [], cx).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await;
thread
.update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx))
@@ -40,6 +40,8 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont
AgentThreadEntry::AssistantMessage(_)
));
});
drop(tempdir);
}
pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
@@ -118,7 +120,7 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await;
thread
.update(cx, |thread, cx| {
@@ -156,8 +158,9 @@ pub async fn test_tool_call_with_permission(
cx: &mut TestAppContext,
) {
let fs = init_test(cx).await;
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
let tempdir = tempfile::tempdir().unwrap();
let project = Project::test(fs, [tempdir.path()], cx).await;
let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await;
let full_turn = thread.update(cx, |thread, cx| {
thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
@@ -239,14 +242,16 @@ pub async fn test_tool_call_with_permission(
"Expected content to contain 'Hello'"
);
});
drop(tempdir);
}
pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
let fs = init_test(cx).await;
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
let full_turn = thread.update(cx, |thread, cx| {
let tempdir = tempfile::tempdir().unwrap();
let project = Project::test(fs, [tempdir.path()], cx).await;
let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await;
let _ = thread.update(cx, |thread, cx| {
thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
cx,
@@ -285,9 +290,8 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
id.clone()
});
let _ = thread.update(cx, |thread, cx| thread.cancel(cx));
full_turn.await.unwrap();
thread.read_with(cx, |thread, _| {
thread.update(cx, |thread, cx| thread.cancel(cx)).await;
thread.read_with(cx, |thread, _cx| {
let AgentThreadEntry::ToolCall(ToolCall {
status: ToolCallStatus::Canceled,
..
@@ -309,12 +313,15 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
AgentThreadEntry::AssistantMessage(..),
))
});
drop(tempdir);
}
pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
let fs = init_test(cx).await;
let tempdir = tempfile::tempdir().unwrap();
let project = Project::test(fs, [], cx).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await;
thread
.update(cx, |thread, cx| thread.send_raw("Hello from test!", cx))
@@ -330,6 +337,8 @@ pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestA
cx.executor().run_until_parked();
assert!(!weak_thread.is_upgradable());
drop(tempdir);
}
#[macro_export]

View File

@@ -21,10 +21,10 @@ use editor::{
use file_icons::FileIcons;
use gpui::{
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, PlatformDisplay, SharedString,
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, linear_gradient,
list, percentage, point, prelude::*, pulsating_between,
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay,
SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement,
Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop,
linear_gradient, list, percentage, point, prelude::*, pulsating_between,
};
use language::language_settings::SoftWrap;
use language::{Buffer, Language};
@@ -34,7 +34,9 @@ use project::Project;
use settings::Settings as _;
use text::{Anchor, BufferSnapshot};
use theme::ThemeSettings;
use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*};
use ui::{
Disclosure, Divider, DividerColor, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*,
};
use util::ResultExt;
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
@@ -69,6 +71,7 @@ pub struct AcpThreadView {
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
last_error: Option<Entity<Markdown>>,
list_state: ListState,
scrollbar_state: ScrollbarState,
auth_task: Option<Task<()>>,
expanded_tool_calls: HashSet<acp::ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
@@ -187,7 +190,8 @@ impl AcpThreadView {
notifications: Vec::new(),
notification_subscriptions: HashMap::default(),
diff_editors: Default::default(),
list_state: list_state,
list_state: list_state.clone(),
scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
last_error: None,
auth_task: None,
expanded_tool_calls: HashSet::default(),
@@ -854,6 +858,7 @@ impl AcpThreadView {
.into_any()
}
AgentThreadEntry::ToolCall(tool_call) => div()
.w_full()
.py_1p5()
.px_5()
.child(self.render_tool_call(index, tool_call, window, cx))
@@ -866,6 +871,7 @@ impl AcpThreadView {
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if index == total_entries - 1 && !is_generating {
v_flex()
.w_full()
.child(primary)
.child(self.render_thread_controls(cx))
.into_any_element()
@@ -898,6 +904,7 @@ impl AcpThreadView {
cx: &Context<Self>,
) -> AnyElement {
let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
let card_header_id = SharedString::from("inner-card-header");
let key = (entry_ix, chunk_ix);
let is_open = self.expanded_thinking_blocks.contains(&key);
@@ -905,41 +912,53 @@ impl AcpThreadView {
.child(
h_flex()
.id(header_id)
.group("disclosure-header")
.group(&card_header_id)
.relative()
.w_full()
.justify_between()
.gap_1p5()
.opacity(0.8)
.hover(|style| style.opacity(1.))
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::ToolBulb)
.size(IconSize::Small)
.color(Color::Muted),
)
.size_4()
.justify_center()
.child(
div()
.text_size(self.tool_name_font_size())
.child("Thinking"),
.group_hover(&card_header_id, |s| s.invisible().w_0())
.child(
Icon::new(IconName::ToolThink)
.size(IconSize::Small)
.color(Color::Muted),
),
)
.child(
h_flex()
.absolute()
.inset_0()
.invisible()
.justify_center()
.group_hover(&card_header_id, |s| s.visible())
.child(
Disclosure::new(("expand", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronRight)
.on_click(cx.listener({
move |this, _event, _window, cx| {
if is_open {
this.expanded_thinking_blocks.remove(&key);
} else {
this.expanded_thinking_blocks.insert(key);
}
cx.notify();
}
})),
),
),
)
.child(
div().visible_on_hover("disclosure-header").child(
Disclosure::new("thinking-disclosure", is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener({
move |this, _event, _window, cx| {
if is_open {
this.expanded_thinking_blocks.remove(&key);
} else {
this.expanded_thinking_blocks.insert(key);
}
cx.notify();
}
})),
),
div()
.text_size(self.tool_name_font_size())
.child("Thinking"),
)
.on_click(cx.listener({
move |this, _event, _window, cx| {
@@ -970,6 +989,67 @@ impl AcpThreadView {
.into_any_element()
}
fn render_tool_call_icon(
&self,
group_name: SharedString,
entry_ix: usize,
is_collapsible: bool,
is_open: bool,
tool_call: &ToolCall,
cx: &Context<Self>,
) -> Div {
let tool_icon = Icon::new(match tool_call.kind {
acp::ToolKind::Read => IconName::ToolRead,
acp::ToolKind::Edit => IconName::ToolPencil,
acp::ToolKind::Delete => IconName::ToolDeleteFile,
acp::ToolKind::Move => IconName::ArrowRightLeft,
acp::ToolKind::Search => IconName::ToolSearch,
acp::ToolKind::Execute => IconName::ToolTerminal,
acp::ToolKind::Think => IconName::ToolThink,
acp::ToolKind::Fetch => IconName::ToolWeb,
acp::ToolKind::Other => IconName::ToolHammer,
})
.size(IconSize::Small)
.color(Color::Muted);
if is_collapsible {
h_flex()
.size_4()
.justify_center()
.child(
div()
.group_hover(&group_name, |s| s.invisible().w_0())
.child(tool_icon),
)
.child(
h_flex()
.absolute()
.inset_0()
.invisible()
.justify_center()
.group_hover(&group_name, |s| s.visible())
.child(
Disclosure::new(("expand", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronRight)
.on_click(cx.listener({
let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
if is_open {
this.expanded_tool_calls.remove(&id);
} else {
this.expanded_tool_calls.insert(id.clone());
}
cx.notify();
}
})),
),
)
} else {
div().child(tool_icon)
}
}
fn render_tool_call(
&self,
entry_ix: usize,
@@ -977,7 +1057,8 @@ impl AcpThreadView {
window: &Window,
cx: &Context<Self>,
) -> Div {
let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
let card_header_id = SharedString::from("inner-tool-call-header");
let status_icon = match &tool_call.status {
ToolCallStatus::Allowed {
@@ -1026,6 +1107,21 @@ impl AcpThreadView {
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
let gradient_color = cx.theme().colors().panel_background;
let gradient_overlay = {
div()
.absolute()
.top_0()
.right_0()
.w_12()
.h_full()
.bg(linear_gradient(
90.,
linear_color_stop(gradient_color, 1.),
linear_color_stop(gradient_color.opacity(0.2), 0.),
))
};
v_flex()
.when(needs_confirmation, |this| {
this.rounded_lg()
@@ -1042,43 +1138,38 @@ impl AcpThreadView {
.justify_between()
.map(|this| {
if needs_confirmation {
this.px_2()
this.pl_2()
.pr_1()
.py_1()
.rounded_t_md()
.bg(self.tool_card_header_bg(cx))
.border_b_1()
.border_color(self.tool_card_border_color(cx))
.bg(self.tool_card_header_bg(cx))
} else {
this.opacity(0.8).hover(|style| style.opacity(1.))
}
})
.child(
h_flex()
.id("tool-call-header")
.overflow_x_scroll()
.group(&card_header_id)
.relative()
.w_full()
.map(|this| {
if needs_confirmation {
this.text_xs()
if tool_call.locations.len() == 1 {
this.gap_0()
} else {
this.text_size(self.tool_name_font_size())
this.gap_1p5()
}
})
.gap_1p5()
.child(
Icon::new(match tool_call.kind {
acp::ToolKind::Read => IconName::ToolRead,
acp::ToolKind::Edit => IconName::ToolPencil,
acp::ToolKind::Delete => IconName::ToolDeleteFile,
acp::ToolKind::Move => IconName::ArrowRightLeft,
acp::ToolKind::Search => IconName::ToolSearch,
acp::ToolKind::Execute => IconName::ToolTerminal,
acp::ToolKind::Think => IconName::ToolBulb,
acp::ToolKind::Fetch => IconName::ToolWeb,
acp::ToolKind::Other => IconName::ToolHammer,
})
.size(IconSize::Small)
.color(Color::Muted),
)
.text_size(self.tool_name_font_size())
.child(self.render_tool_call_icon(
card_header_id,
entry_ix,
is_collapsible,
is_open,
tool_call,
cx,
))
.child(if tool_call.locations.len() == 1 {
let name = tool_call.locations[0]
.path
@@ -1089,13 +1180,11 @@ impl AcpThreadView {
h_flex()
.id(("open-tool-call-location", entry_ix))
.child(name)
.w_full()
.max_w_full()
.pr_1()
.gap_0p5()
.cursor_pointer()
.px_1p5()
.rounded_sm()
.overflow_x_scroll()
.opacity(0.8)
.hover(|label| {
label.opacity(1.).bg(cx
@@ -1104,53 +1193,49 @@ impl AcpThreadView {
.element_hover
.opacity(0.5))
})
.child(name)
.tooltip(Tooltip::text("Jump to File"))
.on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx);
}))
.into_any_element()
} else {
self.render_markdown(
tool_call.label.clone(),
default_markdown_style(needs_confirmation, window, cx),
)
.into_any()
h_flex()
.id("non-card-label-container")
.w_full()
.relative()
.overflow_hidden()
.child(
h_flex()
.id("non-card-label")
.pr_8()
.w_full()
.overflow_x_scroll()
.child(self.render_markdown(
tool_call.label.clone(),
default_markdown_style(
needs_confirmation,
window,
cx,
),
)),
)
.child(gradient_overlay)
.on_click(cx.listener({
let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
if is_open {
this.expanded_tool_calls.remove(&id);
} else {
this.expanded_tool_calls.insert(id.clone());
}
cx.notify();
}
}))
.into_any()
}),
)
.child(
h_flex()
.gap_0p5()
.when(is_collapsible, |this| {
this.child(
Disclosure::new(("expand", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener({
let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
if is_open {
this.expanded_tool_calls.remove(&id);
} else {
this.expanded_tool_calls.insert(id.clone());
}
cx.notify();
}
})),
)
})
.children(status_icon),
)
.on_click(cx.listener({
let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
if is_open {
this.expanded_tool_calls.remove(&id);
} else {
this.expanded_tool_calls.insert(id.clone());
}
cx.notify();
}
})),
.children(status_icon),
)
.when(is_open, |this| {
this.child(
@@ -1244,8 +1329,7 @@ impl AcpThreadView {
cx: &Context<Self>,
) -> Div {
h_flex()
.py_1p5()
.px_1p5()
.p_1p5()
.gap_1()
.justify_end()
.when(!empty_content, |this| {
@@ -1271,6 +1355,7 @@ impl AcpThreadView {
})
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.label_size(LabelSize::Small)
.on_click(cx.listener({
let tool_call_id = tool_call_id.clone();
let option_id = option.id.clone();
@@ -1520,7 +1605,7 @@ impl AcpThreadView {
})
})
.when(!changed_buffers.is_empty(), |this| {
this.child(Divider::horizontal())
this.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_edits_summary(
action_log,
&changed_buffers,
@@ -1550,6 +1635,7 @@ impl AcpThreadView {
{
h_flex()
.w_full()
.cursor_default()
.gap_1()
.text_xs()
.text_color(cx.theme().colors().text_muted)
@@ -1579,7 +1665,7 @@ impl AcpThreadView {
let status_label = if stats.pending == 0 {
"All Done".to_string()
} else if stats.completed == 0 {
format!("{}", plan.entries.len())
format!("{} Tasks", plan.entries.len())
} else {
format!("{}/{}", stats.completed, plan.entries.len())
};
@@ -1693,7 +1779,6 @@ impl AcpThreadView {
.child(
h_flex()
.id("edits-container")
.cursor_pointer()
.w_full()
.gap_1()
.child(Disclosure::new("edits-disclosure", expanded))
@@ -2468,6 +2553,7 @@ impl AcpThreadView {
}));
h_flex()
.w_full()
.mr_1()
.pb_2()
.px(RESPONSE_PADDING_X)
@@ -2478,6 +2564,39 @@ impl AcpThreadView {
.child(open_as_markdown)
.child(scroll_to_top)
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.id("acp-thread-scrollbar")
.occlude()
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
}
}
impl Focusable for AcpThreadView {
@@ -2552,6 +2671,7 @@ impl Render for AcpThreadView {
.flex_grow()
.into_any(),
)
.child(self.render_vertical_scrollbar(cx))
.children(match thread_clone.read(cx).status() {
ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => {
None

View File

@@ -69,8 +69,6 @@ pub struct ActiveThread {
messages: Vec<MessageId>,
list_state: ListState,
scrollbar_state: ScrollbarState,
show_scrollbar: bool,
hide_scrollbar_task: Option<Task<()>>,
rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>,
editing_message: Option<(MessageId, EditingMessageState)>,
@@ -805,9 +803,7 @@ impl ActiveThread {
expanded_thinking_segments: HashMap::default(),
expanded_code_blocks: HashMap::default(),
list_state: list_state.clone(),
scrollbar_state: ScrollbarState::new(list_state),
show_scrollbar: false,
hide_scrollbar_task: None,
scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
editing_message: None,
last_error: None,
copied_code_block_ids: HashSet::default(),
@@ -2628,7 +2624,7 @@ impl ActiveThread {
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::ToolBulb)
Icon::new(IconName::ToolThink)
.size(IconSize::Small)
.color(Color::Muted),
)
@@ -3502,60 +3498,37 @@ impl ActiveThread {
}
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !self.show_scrollbar && !self.scrollbar_state.is_dragging() {
return None;
}
Some(
div()
.occlude()
.id("active-thread-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.occlude()
.id("active-thread-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
}
fn hide_scrollbar_later(&mut self, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn(async move |thread, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
thread
.update(cx, |thread, cx| {
if !thread.scrollbar_state.is_dragging() {
thread.show_scrollbar = false;
cx.notify();
}
})
.log_err();
}))
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
}
pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool {
@@ -3596,26 +3569,8 @@ impl Render for ActiveThread {
.size_full()
.relative()
.bg(cx.theme().colors().panel_background)
.on_mouse_move(cx.listener(|this, _, _, cx| {
this.show_scrollbar = true;
this.hide_scrollbar_later(cx);
cx.notify();
}))
.on_scroll_wheel(cx.listener(|this, _, _, cx| {
this.show_scrollbar = true;
this.hide_scrollbar_later(cx);
cx.notify();
}))
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, _, cx| {
this.hide_scrollbar_later(cx);
}),
)
.child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow())
.when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
this.child(scrollbar)
})
.child(self.render_vertical_scrollbar(cx))
}
}

View File

@@ -37,7 +37,7 @@ impl Tool for ThinkingTool {
}
fn icon(&self) -> IconName {
IconName::ToolBulb
IconName::ToolThink
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -16,7 +16,6 @@ use clock::SystemClock;
use cloud_api_client::CloudApiClient;
use cloud_api_client::websocket_protocol::MessageToClient;
use credentials_provider::CredentialsProvider;
use feature_flags::FeatureFlagAppExt as _;
use futures::{
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
channel::oneshot, future::BoxFuture,
@@ -193,8 +192,6 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
});
}
pub type MessageToClientHandler = Box<dyn Fn(&MessageToClient, &mut App) + Send + Sync + 'static>;
struct GlobalClient(Arc<Client>);
impl Global for GlobalClient {}
@@ -208,7 +205,6 @@ pub struct Client {
credentials_provider: ClientCredentialsProvider,
state: RwLock<ClientState>,
handler_set: parking_lot::Mutex<ProtoMessageHandlerSet>,
message_to_client_handlers: parking_lot::Mutex<Vec<MessageToClientHandler>>,
#[allow(clippy::type_complexity)]
#[cfg(any(test, feature = "test-support"))]
@@ -558,7 +554,6 @@ impl Client {
credentials_provider: ClientCredentialsProvider::new(cx),
state: Default::default(),
handler_set: Default::default(),
message_to_client_handlers: parking_lot::Mutex::new(Vec::new()),
#[cfg(any(test, feature = "test-support"))]
authenticate: Default::default(),
@@ -965,51 +960,25 @@ impl Client {
Ok(())
}
/// Performs a sign-in and also (optionally) connects to Collab.
/// Performs a sign-in and also connects to Collab.
///
/// Only Zed staff automatically connect to Collab.
/// This is called in places where we *don't* need to connect in the future. We will replace these calls with calls
/// to `sign_in` when we're ready to remove auto-connection to Collab.
pub async fn sign_in_with_optional_connect(
self: &Arc<Self>,
try_provider: bool,
cx: &AsyncApp,
) -> Result<()> {
let (is_staff_tx, is_staff_rx) = oneshot::channel::<bool>();
let mut is_staff_tx = Some(is_staff_tx);
cx.update(|cx| {
cx.on_flags_ready(move |state, _cx| {
if let Some(is_staff_tx) = is_staff_tx.take() {
is_staff_tx.send(state.is_staff).log_err();
}
})
.detach();
})
.log_err();
let credentials = self.sign_in(try_provider, cx).await?;
self.connect_to_cloud(cx).await.log_err();
cx.update(move |cx| {
cx.spawn({
let client = self.clone();
async move |cx| {
let is_staff = is_staff_rx.await?;
if is_staff {
match client.connect_with_credentials(credentials, cx).await {
ConnectionResult::Timeout => Err(anyhow!("connection timed out")),
ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")),
ConnectionResult::Result(result) => {
result.context("client auth and connect")
}
}
} else {
Ok(())
}
}
})
.detach_and_log_err(cx);
})
.log_err();
let connect_result = match self.connect_with_credentials(credentials, cx).await {
ConnectionResult::Timeout => Err(anyhow!("connection timed out")),
ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")),
ConnectionResult::Result(result) => result.context("client auth and connect"),
};
connect_result.log_err();
Ok(())
}
@@ -1682,22 +1651,10 @@ impl Client {
}
}
pub fn add_message_to_client_handler(
self: &Arc<Client>,
handler: impl Fn(&MessageToClient, &mut App) + Send + Sync + 'static,
) {
self.message_to_client_handlers
.lock()
.push(Box::new(handler));
}
fn handle_message_to_client(self: &Arc<Client>, message: MessageToClient, cx: &AsyncApp) {
cx.update(|cx| {
for handler in self.message_to_client_handlers.lock().iter() {
handler(&message, cx);
}
})
.ok();
fn handle_message_to_client(self: &Arc<Client>, message: MessageToClient, _cx: &AsyncApp) {
match message {
MessageToClient::UserUpdated => {}
}
}
pub fn telemetry(&self) -> &Arc<Telemetry> {

View File

@@ -1,7 +1,6 @@
use super::{Client, Status, TypedEnvelope, proto};
use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
use cloud_api_client::websocket_protocol::MessageToClient;
use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo};
use cloud_llm_client::{
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
@@ -182,12 +181,6 @@ impl UserStore {
client.add_message_handler(cx.weak_entity(), Self::handle_update_invite_info),
client.add_message_handler(cx.weak_entity(), Self::handle_show_contacts),
];
client.add_message_to_client_handler({
let this = cx.weak_entity();
move |message, cx| Self::handle_message_to_client(this.clone(), message, cx)
});
Self {
users: Default::default(),
by_github_login: Default::default(),
@@ -226,35 +219,17 @@ impl UserStore {
match status {
Status::Authenticated | Status::Connected { .. } => {
if let Some(user_id) = client.user_id() {
let response = client
.cloud_client()
.get_authenticated_user()
.await
.log_err();
let current_user_and_response = if let Some(response) = response {
let user = Arc::new(User {
id: user_id,
github_login: response.user.github_login.clone().into(),
avatar_uri: response.user.avatar_url.clone().into(),
name: response.user.name.clone(),
});
Some((user, response))
} else {
None
};
current_user_tx
.send(
current_user_and_response
.as_ref()
.map(|(user, _)| user.clone()),
)
.await
.ok();
let response = client.cloud_client().get_authenticated_user().await;
let mut current_user = None;
cx.update(|cx| {
if let Some((user, response)) = current_user_and_response {
if let Some(response) = response.log_err() {
let user = Arc::new(User {
id: user_id,
github_login: response.user.github_login.clone().into(),
avatar_uri: response.user.avatar_url.clone().into(),
name: response.user.name.clone(),
});
current_user = Some(user.clone());
this.update(cx, |this, cx| {
this.by_github_login
.insert(user.github_login.clone(), user_id);
@@ -265,6 +240,7 @@ impl UserStore {
anyhow::Ok(())
}
})??;
current_user_tx.send(current_user).await.ok();
this.update(cx, |_, cx| cx.notify())?;
}
@@ -837,32 +813,6 @@ impl UserStore {
cx.emit(Event::PrivateUserInfoUpdated);
}
fn handle_message_to_client(this: WeakEntity<Self>, message: &MessageToClient, cx: &App) {
cx.spawn(async move |cx| {
match message {
MessageToClient::UserUpdated => {
let cloud_client = cx
.update(|cx| {
this.read_with(cx, |this, _cx| {
this.client.upgrade().map(|client| client.cloud_client())
})
})??
.ok_or(anyhow::anyhow!("Failed to get Cloud client"))?;
let response = cloud_client.get_authenticated_user().await?;
cx.update(|cx| {
this.update(cx, |this, cx| {
this.update_authenticated_user(response, cx);
})
})??;
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
self.current_user.clone()
}

View File

@@ -1,477 +1,10 @@
use anyhow::{Context as _, bail};
use chrono::{DateTime, Utc};
use sea_orm::ActiveValue;
use std::{sync::Arc, time::Duration};
use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus};
use util::ResultExt;
use std::sync::Arc;
use stripe::SubscriptionStatus;
use crate::AppState;
use crate::db::billing_subscription::{
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
};
use crate::db::{
CreateBillingCustomerParams, CreateBillingSubscriptionParams, CreateProcessedStripeEventParams,
UpdateBillingCustomerParams, UpdateBillingSubscriptionParams, billing_customer,
};
use crate::rpc::{ResultExt as _, Server};
use crate::stripe_client::{
StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription,
StripeSubscriptionId,
};
/// The amount of time we wait in between each poll of Stripe events.
///
/// This value should strike a balance between:
/// 1. Being short enough that we update quickly when something in Stripe changes
/// 2. Being long enough that we don't eat into our rate limits.
///
/// As a point of reference, the Sequin folks say they have this at **500ms**:
///
/// > We poll the Stripe /events endpoint every 500ms per account
/// >
/// > — https://blog.sequinstream.com/events-not-webhooks/
const POLL_EVENTS_INTERVAL: Duration = Duration::from_secs(5);
/// The maximum number of events to return per page.
///
/// We set this to 100 (the max) so we have to make fewer requests to Stripe.
///
/// > Limit can range between 1 and 100, and the default is 10.
const EVENTS_LIMIT_PER_PAGE: u64 = 100;
/// The number of pages consisting entirely of already-processed events that we
/// will see before we stop retrieving events.
///
/// This is used to prevent over-fetching the Stripe events API for events we've
/// already seen and processed.
const NUMBER_OF_ALREADY_PROCESSED_PAGES_BEFORE_WE_STOP: usize = 4;
/// Polls the Stripe events API periodically to reconcile the records in our
/// database with the data in Stripe.
pub fn poll_stripe_events_periodically(app: Arc<AppState>, rpc_server: Arc<Server>) {
let Some(real_stripe_client) = app.real_stripe_client.clone() else {
log::warn!("failed to retrieve Stripe client");
return;
};
let Some(stripe_client) = app.stripe_client.clone() else {
log::warn!("failed to retrieve Stripe client");
return;
};
let executor = app.executor.clone();
executor.spawn_detached({
let executor = executor.clone();
async move {
loop {
poll_stripe_events(&app, &rpc_server, &stripe_client, &real_stripe_client)
.await
.log_err();
executor.sleep(POLL_EVENTS_INTERVAL).await;
}
}
});
}
async fn poll_stripe_events(
app: &Arc<AppState>,
rpc_server: &Arc<Server>,
stripe_client: &Arc<dyn StripeClient>,
real_stripe_client: &stripe::Client,
) -> anyhow::Result<()> {
let feature_flags = app.db.list_feature_flags().await?;
let sync_events_using_cloud = feature_flags
.iter()
.any(|flag| flag.flag == "cloud-stripe-events-polling" && flag.enabled_for_all);
if sync_events_using_cloud {
return Ok(());
}
fn event_type_to_string(event_type: EventType) -> String {
// Calling `to_string` on `stripe::EventType` members gives us a quoted string,
// so we need to unquote it.
event_type.to_string().trim_matches('"').to_string()
}
let event_types = [
EventType::CustomerCreated,
EventType::CustomerUpdated,
EventType::CustomerSubscriptionCreated,
EventType::CustomerSubscriptionUpdated,
EventType::CustomerSubscriptionPaused,
EventType::CustomerSubscriptionResumed,
EventType::CustomerSubscriptionDeleted,
]
.into_iter()
.map(event_type_to_string)
.collect::<Vec<_>>();
let mut pages_of_already_processed_events = 0;
let mut unprocessed_events = Vec::new();
log::info!(
"Stripe events: starting retrieval for {}",
event_types.join(", ")
);
let mut params = ListEvents::new();
params.types = Some(event_types.clone());
params.limit = Some(EVENTS_LIMIT_PER_PAGE);
let mut event_pages = stripe::Event::list(&real_stripe_client, &params)
.await?
.paginate(params);
loop {
let processed_event_ids = {
let event_ids = event_pages
.page
.data
.iter()
.map(|event| event.id.as_str())
.collect::<Vec<_>>();
app.db
.get_processed_stripe_events_by_event_ids(&event_ids)
.await?
.into_iter()
.map(|event| event.stripe_event_id)
.collect::<Vec<_>>()
};
let mut processed_events_in_page = 0;
let events_in_page = event_pages.page.data.len();
for event in &event_pages.page.data {
if processed_event_ids.contains(&event.id.to_string()) {
processed_events_in_page += 1;
log::debug!("Stripe events: already processed '{}', skipping", event.id);
} else {
unprocessed_events.push(event.clone());
}
}
if processed_events_in_page == events_in_page {
pages_of_already_processed_events += 1;
}
if event_pages.page.has_more {
if pages_of_already_processed_events >= NUMBER_OF_ALREADY_PROCESSED_PAGES_BEFORE_WE_STOP
{
log::info!(
"Stripe events: stopping, saw {pages_of_already_processed_events} pages of already-processed events"
);
break;
} else {
log::info!("Stripe events: retrieving next page");
event_pages = event_pages.next(&real_stripe_client).await?;
}
} else {
break;
}
}
log::info!("Stripe events: unprocessed {}", unprocessed_events.len());
// Sort all of the unprocessed events in ascending order, so we can handle them in the order they occurred.
unprocessed_events.sort_by(|a, b| a.created.cmp(&b.created).then_with(|| a.id.cmp(&b.id)));
for event in unprocessed_events {
let event_id = event.id.clone();
let processed_event_params = CreateProcessedStripeEventParams {
stripe_event_id: event.id.to_string(),
stripe_event_type: event_type_to_string(event.type_),
stripe_event_created_timestamp: event.created,
};
// If the event has happened too far in the past, we don't want to
// process it and risk overwriting other more-recent updates.
//
// 1 day was chosen arbitrarily. This could be made longer or shorter.
let one_day = Duration::from_secs(24 * 60 * 60);
let a_day_ago = Utc::now() - one_day;
if a_day_ago.timestamp() > event.created {
log::info!(
"Stripe events: event '{}' is more than {one_day:?} old, marking as processed",
event_id
);
app.db
.create_processed_stripe_event(&processed_event_params)
.await?;
continue;
}
let process_result = match event.type_ {
EventType::CustomerCreated | EventType::CustomerUpdated => {
handle_customer_event(app, real_stripe_client, event).await
}
EventType::CustomerSubscriptionCreated
| EventType::CustomerSubscriptionUpdated
| EventType::CustomerSubscriptionPaused
| EventType::CustomerSubscriptionResumed
| EventType::CustomerSubscriptionDeleted => {
handle_customer_subscription_event(app, rpc_server, stripe_client, event).await
}
_ => Ok(()),
};
if let Some(()) = process_result
.with_context(|| format!("failed to process event {event_id} successfully"))
.log_err()
{
app.db
.create_processed_stripe_event(&processed_event_params)
.await?;
}
}
Ok(())
}
async fn handle_customer_event(
app: &Arc<AppState>,
_stripe_client: &stripe::Client,
event: stripe::Event,
) -> anyhow::Result<()> {
let EventObject::Customer(customer) = event.data.object else {
bail!("unexpected event payload for {}", event.id);
};
log::info!("handling Stripe {} event: {}", event.type_, event.id);
let Some(email) = customer.email else {
log::info!("Stripe customer has no email: skipping");
return Ok(());
};
let Some(user) = app.db.get_user_by_email(&email).await? else {
log::info!("no user found for email: skipping");
return Ok(());
};
if let Some(existing_customer) = app
.db
.get_billing_customer_by_stripe_customer_id(&customer.id)
.await?
{
app.db
.update_billing_customer(
existing_customer.id,
&UpdateBillingCustomerParams {
// For now we just leave the information as-is, as it is not
// likely to change.
..Default::default()
},
)
.await?;
} else {
app.db
.create_billing_customer(&CreateBillingCustomerParams {
user_id: user.id,
stripe_customer_id: customer.id.to_string(),
})
.await?;
}
Ok(())
}
async fn sync_subscription(
app: &Arc<AppState>,
stripe_client: &Arc<dyn StripeClient>,
subscription: StripeSubscription,
) -> anyhow::Result<billing_customer::Model> {
let subscription_kind = if let Some(stripe_billing) = &app.stripe_billing {
stripe_billing
.determine_subscription_kind(&subscription)
.await
} else {
None
};
let billing_customer =
find_or_create_billing_customer(app, stripe_client.as_ref(), &subscription.customer)
.await?
.context("billing customer not found")?;
if let Some(SubscriptionKind::ZedProTrial) = subscription_kind {
if subscription.status == SubscriptionStatus::Trialing {
let current_period_start =
DateTime::from_timestamp(subscription.current_period_start, 0)
.context("No trial subscription period start")?;
app.db
.update_billing_customer(
billing_customer.id,
&UpdateBillingCustomerParams {
trial_started_at: ActiveValue::set(Some(current_period_start.naive_utc())),
..Default::default()
},
)
.await?;
}
}
let was_canceled_due_to_payment_failure = subscription.status == SubscriptionStatus::Canceled
&& subscription
.cancellation_details
.as_ref()
.and_then(|details| details.reason)
.map_or(false, |reason| {
reason == StripeCancellationDetailsReason::PaymentFailed
});
if was_canceled_due_to_payment_failure {
app.db
.update_billing_customer(
billing_customer.id,
&UpdateBillingCustomerParams {
has_overdue_invoices: ActiveValue::set(true),
..Default::default()
},
)
.await?;
}
if let Some(existing_subscription) = app
.db
.get_billing_subscription_by_stripe_subscription_id(subscription.id.0.as_ref())
.await?
{
app.db
.update_billing_subscription(
existing_subscription.id,
&UpdateBillingSubscriptionParams {
billing_customer_id: ActiveValue::set(billing_customer.id),
kind: ActiveValue::set(subscription_kind),
stripe_subscription_id: ActiveValue::set(subscription.id.to_string()),
stripe_subscription_status: ActiveValue::set(subscription.status.into()),
stripe_cancel_at: ActiveValue::set(
subscription
.cancel_at
.and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0))
.map(|time| time.naive_utc()),
),
stripe_cancellation_reason: ActiveValue::set(
subscription
.cancellation_details
.and_then(|details| details.reason)
.map(|reason| reason.into()),
),
stripe_current_period_start: ActiveValue::set(Some(
subscription.current_period_start,
)),
stripe_current_period_end: ActiveValue::set(Some(
subscription.current_period_end,
)),
},
)
.await?;
} else {
if let Some(existing_subscription) = app
.db
.get_active_billing_subscription(billing_customer.user_id)
.await?
{
if existing_subscription.kind == Some(SubscriptionKind::ZedFree)
&& subscription_kind == Some(SubscriptionKind::ZedProTrial)
{
let stripe_subscription_id = StripeSubscriptionId(
existing_subscription.stripe_subscription_id.clone().into(),
);
stripe_client
.cancel_subscription(&stripe_subscription_id)
.await?;
} else {
// If the user already has an active billing subscription, ignore the
// event and return an `Ok` to signal that it was processed
// successfully.
//
// There is the possibility that this could cause us to not create a
// subscription in the following scenario:
//
// 1. User has an active subscription A
// 2. User cancels subscription A
// 3. User creates a new subscription B
// 4. We process the new subscription B before the cancellation of subscription A
// 5. User ends up with no subscriptions
//
// In theory this situation shouldn't arise as we try to process the events in the order they occur.
log::info!(
"user {user_id} already has an active subscription, skipping creation of subscription {subscription_id}",
user_id = billing_customer.user_id,
subscription_id = subscription.id
);
return Ok(billing_customer);
}
}
app.db
.create_billing_subscription(&CreateBillingSubscriptionParams {
billing_customer_id: billing_customer.id,
kind: subscription_kind,
stripe_subscription_id: subscription.id.to_string(),
stripe_subscription_status: subscription.status.into(),
stripe_cancellation_reason: subscription
.cancellation_details
.and_then(|details| details.reason)
.map(|reason| reason.into()),
stripe_current_period_start: Some(subscription.current_period_start),
stripe_current_period_end: Some(subscription.current_period_end),
})
.await?;
}
if let Some(stripe_billing) = app.stripe_billing.as_ref() {
if subscription.status == SubscriptionStatus::Canceled
|| subscription.status == SubscriptionStatus::Paused
{
let already_has_active_billing_subscription = app
.db
.has_active_billing_subscription(billing_customer.user_id)
.await?;
if !already_has_active_billing_subscription {
let stripe_customer_id =
StripeCustomerId(billing_customer.stripe_customer_id.clone().into());
stripe_billing
.subscribe_to_zed_free(stripe_customer_id)
.await?;
}
}
}
Ok(billing_customer)
}
async fn handle_customer_subscription_event(
app: &Arc<AppState>,
rpc_server: &Arc<Server>,
stripe_client: &Arc<dyn StripeClient>,
event: stripe::Event,
) -> anyhow::Result<()> {
let EventObject::Subscription(subscription) = event.data.object else {
bail!("unexpected event payload for {}", event.id);
};
log::info!("handling Stripe {} event: {}", event.type_, event.id);
let billing_customer = sync_subscription(app, stripe_client, subscription.into()).await?;
// When the user's subscription changes, push down any changes to their plan.
rpc_server
.update_plan_for_user_legacy(billing_customer.user_id)
.await
.trace_err();
// When the user's subscription changes, we want to refresh their LLM tokens
// to either grant/revoke access.
rpc_server
.refresh_llm_tokens_for_user(billing_customer.user_id)
.await;
Ok(())
}
use crate::db::billing_subscription::StripeSubscriptionStatus;
use crate::db::{CreateBillingCustomerParams, billing_customer};
use crate::stripe_client::{StripeClient, StripeCustomerId};
impl From<SubscriptionStatus> for StripeSubscriptionStatus {
fn from(value: SubscriptionStatus) -> Self {
@@ -488,16 +21,6 @@ impl From<SubscriptionStatus> for StripeSubscriptionStatus {
}
}
impl From<CancellationDetailsReason> for StripeCancellationReason {
fn from(value: CancellationDetailsReason) -> Self {
match value {
CancellationDetailsReason::CancellationRequested => Self::CancellationRequested,
CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed,
CancellationDetailsReason::PaymentFailed => Self::PaymentFailed,
}
}
}
/// Finds or creates a billing customer using the provided customer.
pub async fn find_or_create_billing_customer(
app: &Arc<AppState>,

View File

@@ -699,10 +699,7 @@ impl Database {
language_server::Column::ProjectId,
language_server::Column::Id,
])
.update_columns([
language_server::Column::Name,
language_server::Column::Capabilities,
])
.update_column(language_server::Column::Name)
.to_owned(),
)
.exec(&*tx)

View File

@@ -7,6 +7,7 @@ use axum::{
routing::get,
};
use collab::ServiceMode;
use collab::api::CloudflareIpCountryHeader;
use collab::llm::db::LlmDatabase;
use collab::migrations::run_database_migrations;
@@ -15,7 +16,6 @@ use collab::{
AppState, Config, Result, api::fetch_extensions_from_blob_store_periodically, db, env,
executor::Executor, rpc::ResultExt,
};
use collab::{ServiceMode, api::billing::poll_stripe_events_periodically};
use db::Database;
use std::{
env::args,
@@ -119,8 +119,6 @@ async fn main() -> Result<()> {
let rpc_server = collab::rpc::Server::new(epoch, state.clone());
rpc_server.start().await?;
poll_stripe_events_periodically(state.clone(), rpc_server.clone());
app = app
.merge(collab::api::routes(rpc_server.clone()))
.merge(collab::rpc::routes(rpc_server.clone()));

View File

@@ -746,6 +746,7 @@ impl Server {
address: String,
principal: Principal,
zed_version: ZedVersion,
release_channel: Option<String>,
user_agent: Option<String>,
geoip_country_code: Option<String>,
system_id: Option<String>,
@@ -766,6 +767,9 @@ impl Server {
if let Some(user_agent) = user_agent {
span.record("user_agent", user_agent);
}
if let Some(release_channel) = release_channel {
span.record("release_channel", release_channel);
}
if let Some(country_code) = geoip_country_code.as_ref() {
span.record("geoip_country_code", country_code);
@@ -1181,6 +1185,35 @@ impl Header for AppVersionHeader {
}
}
#[derive(Debug)]
pub struct ReleaseChannelHeader(String);
impl Header for ReleaseChannelHeader {
fn name() -> &'static HeaderName {
static ZED_RELEASE_CHANNEL: OnceLock<HeaderName> = OnceLock::new();
ZED_RELEASE_CHANNEL.get_or_init(|| HeaderName::from_static("x-zed-release-channel"))
}
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
where
Self: Sized,
I: Iterator<Item = &'i axum::http::HeaderValue>,
{
Ok(Self(
values
.next()
.ok_or_else(axum::headers::Error::invalid)?
.to_str()
.map_err(|_| axum::headers::Error::invalid())?
.to_owned(),
))
}
fn encode<E: Extend<axum::http::HeaderValue>>(&self, values: &mut E) {
values.extend([self.0.parse().unwrap()]);
}
}
pub fn routes(server: Arc<Server>) -> Router<(), Body> {
Router::new()
.route("/rpc", get(handle_websocket_request))
@@ -1196,6 +1229,7 @@ pub fn routes(server: Arc<Server>) -> Router<(), Body> {
pub async fn handle_websocket_request(
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
app_version_header: Option<TypedHeader<AppVersionHeader>>,
release_channel_header: Option<TypedHeader<ReleaseChannelHeader>>,
ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
Extension(server): Extension<Arc<Server>>,
Extension(principal): Extension<Principal>,
@@ -1220,6 +1254,8 @@ pub async fn handle_websocket_request(
.into_response();
};
let release_channel = release_channel_header.map(|header| header.0.0);
if !version.can_collaborate() {
return (
StatusCode::UPGRADE_REQUIRED,
@@ -1255,6 +1291,7 @@ pub async fn handle_websocket_request(
socket_address,
principal,
version,
release_channel,
user_agent.map(|header| header.to_string()),
country_code_header.map(|header| header.to_string()),
system_id_header.map(|header| header.to_string()),

View File

@@ -1,21 +1,15 @@
use std::sync::Arc;
use anyhow::anyhow;
use chrono::Utc;
use collections::HashMap;
use stripe::SubscriptionStatus;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::Result;
use crate::db::billing_subscription::SubscriptionKind;
use crate::stripe_client::{
RealStripeClient, StripeAutomaticTax, StripeClient, StripeCreateMeterEventParams,
StripeCreateMeterEventPayload, StripeCreateSubscriptionItems, StripeCreateSubscriptionParams,
StripeCustomerId, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId,
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems,
UpdateSubscriptionParams,
RealStripeClient, StripeAutomaticTax, StripeClient, StripeCreateSubscriptionItems,
StripeCreateSubscriptionParams, StripeCustomerId, StripePrice, StripePriceId,
StripeSubscription,
};
pub struct StripeBilling {
@@ -94,30 +88,6 @@ impl StripeBilling {
.ok_or_else(|| crate::Error::Internal(anyhow!("no price found for {lookup_key:?}")))
}
pub async fn determine_subscription_kind(
&self,
subscription: &StripeSubscription,
) -> Option<SubscriptionKind> {
let zed_pro_price_id = self.zed_pro_price_id().await.ok()?;
let zed_free_price_id = self.zed_free_price_id().await.ok()?;
subscription.items.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
}
})
}
/// Returns the Stripe customer associated with the provided email address, or creates a new customer, if one does
/// not already exist.
///
@@ -150,65 +120,6 @@ impl StripeBilling {
Ok(customer_id)
}
pub async fn subscribe_to_price(
&self,
subscription_id: &StripeSubscriptionId,
price: &StripePrice,
) -> Result<()> {
let subscription = self.client.get_subscription(subscription_id).await?;
if subscription_contains_price(&subscription, &price.id) {
return Ok(());
}
const BILLING_THRESHOLD_IN_CENTS: i64 = 20 * 100;
let price_per_unit = price.unit_amount.unwrap_or_default();
let _units_for_billing_threshold = BILLING_THRESHOLD_IN_CENTS / price_per_unit;
self.client
.update_subscription(
subscription_id,
UpdateSubscriptionParams {
items: Some(vec![UpdateSubscriptionItems {
price: Some(price.id.clone()),
}]),
trial_settings: Some(StripeSubscriptionTrialSettings {
end_behavior: StripeSubscriptionTrialSettingsEndBehavior {
missing_payment_method: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel
},
}),
},
)
.await?;
Ok(())
}
pub async fn bill_model_request_usage(
&self,
customer_id: &StripeCustomerId,
event_name: &str,
requests: i32,
) -> Result<()> {
let timestamp = Utc::now().timestamp();
let idempotency_key = Uuid::new_v4();
self.client
.create_meter_event(StripeCreateMeterEventParams {
identifier: &format!("model_requests/{}", idempotency_key),
event_name,
payload: StripeCreateMeterEventPayload {
value: requests as u64,
stripe_customer_id: customer_id,
},
timestamp: Some(timestamp),
})
.await?;
Ok(())
}
pub async fn subscribe_to_zed_free(
&self,
customer_id: StripeCustomerId,
@@ -243,14 +154,3 @@ impl StripeBilling {
Ok(subscription)
}
}
fn subscription_contains_price(
subscription: &StripeSubscription,
price_id: &StripePriceId,
) -> bool {
subscription.items.iter().any(|item| {
item.price
.as_ref()
.map_or(false, |price| price.id == *price_id)
})
}

View File

@@ -1,14 +1,9 @@
use std::sync::Arc;
use chrono::{Duration, Utc};
use pretty_assertions::assert_eq;
use crate::stripe_billing::StripeBilling;
use crate::stripe_client::{
FakeStripeClient, StripeCustomerId, StripeMeter, StripeMeterId, StripePrice, StripePriceId,
StripePriceRecurring, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem,
StripeSubscriptionItemId, UpdateSubscriptionItems,
};
use crate::stripe_client::{FakeStripeClient, StripePrice, StripePriceId, StripePriceRecurring};
fn make_stripe_billing() -> (StripeBilling, Arc<FakeStripeClient>) {
let stripe_client = Arc::new(FakeStripeClient::new());
@@ -21,24 +16,6 @@ fn make_stripe_billing() -> (StripeBilling, Arc<FakeStripeClient>) {
async fn test_initialize() {
let (stripe_billing, stripe_client) = make_stripe_billing();
// Add test meters
let meter1 = StripeMeter {
id: StripeMeterId("meter_1".into()),
event_name: "event_1".to_string(),
};
let meter2 = StripeMeter {
id: StripeMeterId("meter_2".into()),
event_name: "event_2".to_string(),
};
stripe_client
.meters
.lock()
.insert(meter1.id.clone(), meter1);
stripe_client
.meters
.lock()
.insert(meter2.id.clone(), meter2);
// Add test prices
let price1 = StripePrice {
id: StripePriceId("price_1".into()),
@@ -144,217 +121,3 @@ async fn test_find_or_create_customer_by_email() {
assert_eq!(customer.email.as_deref(), Some(email));
}
}
#[gpui::test]
async fn test_subscribe_to_price() {
let (stripe_billing, stripe_client) = make_stripe_billing();
let price = StripePrice {
id: StripePriceId("price_test".into()),
unit_amount: Some(2000),
lookup_key: Some("test-price".to_string()),
recurring: None,
};
stripe_client
.prices
.lock()
.insert(price.id.clone(), price.clone());
let now = Utc::now();
let subscription = StripeSubscription {
id: StripeSubscriptionId("sub_test".into()),
customer: StripeCustomerId("cus_test".into()),
status: stripe::SubscriptionStatus::Active,
current_period_start: now.timestamp(),
current_period_end: (now + Duration::days(30)).timestamp(),
items: vec![],
cancel_at: None,
cancellation_details: None,
};
stripe_client
.subscriptions
.lock()
.insert(subscription.id.clone(), subscription.clone());
stripe_billing
.subscribe_to_price(&subscription.id, &price)
.await
.unwrap();
let update_subscription_calls = stripe_client
.update_subscription_calls
.lock()
.iter()
.map(|(id, params)| (id.clone(), params.clone()))
.collect::<Vec<_>>();
assert_eq!(update_subscription_calls.len(), 1);
assert_eq!(update_subscription_calls[0].0, subscription.id);
assert_eq!(
update_subscription_calls[0].1.items,
Some(vec![UpdateSubscriptionItems {
price: Some(price.id.clone())
}])
);
// Subscribing to a price that is already on the subscription is a no-op.
{
let now = Utc::now();
let subscription = StripeSubscription {
id: StripeSubscriptionId("sub_test".into()),
customer: StripeCustomerId("cus_test".into()),
status: stripe::SubscriptionStatus::Active,
current_period_start: now.timestamp(),
current_period_end: (now + Duration::days(30)).timestamp(),
items: vec![StripeSubscriptionItem {
id: StripeSubscriptionItemId("si_test".into()),
price: Some(price.clone()),
}],
cancel_at: None,
cancellation_details: None,
};
stripe_client
.subscriptions
.lock()
.insert(subscription.id.clone(), subscription.clone());
stripe_billing
.subscribe_to_price(&subscription.id, &price)
.await
.unwrap();
assert_eq!(stripe_client.update_subscription_calls.lock().len(), 1);
}
}
#[gpui::test]
async fn test_subscribe_to_zed_free() {
let (stripe_billing, stripe_client) = make_stripe_billing();
let zed_pro_price = StripePrice {
id: StripePriceId("price_1".into()),
unit_amount: Some(0),
lookup_key: Some("zed-pro".to_string()),
recurring: None,
};
stripe_client
.prices
.lock()
.insert(zed_pro_price.id.clone(), zed_pro_price.clone());
let zed_free_price = StripePrice {
id: StripePriceId("price_2".into()),
unit_amount: Some(0),
lookup_key: Some("zed-free".to_string()),
recurring: None,
};
stripe_client
.prices
.lock()
.insert(zed_free_price.id.clone(), zed_free_price.clone());
stripe_billing.initialize().await.unwrap();
// Customer is subscribed to Zed Free when not already subscribed to a plan.
{
let customer_id = StripeCustomerId("cus_no_plan".into());
let subscription = stripe_billing
.subscribe_to_zed_free(customer_id)
.await
.unwrap();
assert_eq!(subscription.items[0].price.as_ref(), Some(&zed_free_price));
}
// Customer is not subscribed to Zed Free when they already have an active subscription.
{
let customer_id = StripeCustomerId("cus_active_subscription".into());
let now = Utc::now();
let existing_subscription = StripeSubscription {
id: StripeSubscriptionId("sub_existing_active".into()),
customer: customer_id.clone(),
status: stripe::SubscriptionStatus::Active,
current_period_start: now.timestamp(),
current_period_end: (now + Duration::days(30)).timestamp(),
items: vec![StripeSubscriptionItem {
id: StripeSubscriptionItemId("si_test".into()),
price: Some(zed_pro_price.clone()),
}],
cancel_at: None,
cancellation_details: None,
};
stripe_client.subscriptions.lock().insert(
existing_subscription.id.clone(),
existing_subscription.clone(),
);
let subscription = stripe_billing
.subscribe_to_zed_free(customer_id)
.await
.unwrap();
assert_eq!(subscription, existing_subscription);
}
// Customer is not subscribed to Zed Free when they already have a trial subscription.
{
let customer_id = StripeCustomerId("cus_trial_subscription".into());
let now = Utc::now();
let existing_subscription = StripeSubscription {
id: StripeSubscriptionId("sub_existing_trial".into()),
customer: customer_id.clone(),
status: stripe::SubscriptionStatus::Trialing,
current_period_start: now.timestamp(),
current_period_end: (now + Duration::days(14)).timestamp(),
items: vec![StripeSubscriptionItem {
id: StripeSubscriptionItemId("si_test".into()),
price: Some(zed_pro_price.clone()),
}],
cancel_at: None,
cancellation_details: None,
};
stripe_client.subscriptions.lock().insert(
existing_subscription.id.clone(),
existing_subscription.clone(),
);
let subscription = stripe_billing
.subscribe_to_zed_free(customer_id)
.await
.unwrap();
assert_eq!(subscription, existing_subscription);
}
}
#[gpui::test]
async fn test_bill_model_request_usage() {
let (stripe_billing, stripe_client) = make_stripe_billing();
let customer_id = StripeCustomerId("cus_test".into());
stripe_billing
.bill_model_request_usage(&customer_id, "some_model/requests", 73)
.await
.unwrap();
let create_meter_event_calls = stripe_client
.create_meter_event_calls
.lock()
.iter()
.cloned()
.collect::<Vec<_>>();
assert_eq!(create_meter_event_calls.len(), 1);
assert!(
create_meter_event_calls[0]
.identifier
.starts_with("model_requests/")
);
assert_eq!(create_meter_event_calls[0].stripe_customer_id, customer_id);
assert_eq!(
create_meter_event_calls[0].event_name.as_ref(),
"some_model/requests"
);
assert_eq!(create_meter_event_calls[0].value, 73);
}

View File

@@ -297,6 +297,7 @@ impl TestServer {
client_name,
Principal::User(user),
ZedVersion(SemanticVersion::new(1, 0, 0)),
Some("test".to_string()),
None,
None,
None,

View File

@@ -3053,7 +3053,7 @@ impl Render for CollabPanel {
.on_action(cx.listener(CollabPanel::move_channel_down))
.track_focus(&self.focus_handle)
.size_full()
.child(if !self.client.status().borrow().is_connected() {
.child(if self.user_store.read(cx).current_user().is_none() {
self.render_signed_out(cx)
} else {
self.render_signed_in(window, cx)

View File

@@ -136,7 +136,10 @@ impl Focusable for CommandPalette {
impl Render for CommandPalette {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
v_flex()
.key_context("CommandPalette")
.w(rems(34.))
.child(self.picker.clone())
}
}

View File

@@ -21,7 +21,7 @@ use language::{
point_from_lsp, point_to_lsp,
};
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
use node_runtime::{NodeRuntime, VersionCheck};
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
use project::DisableAiSettings;
use request::StatusNotification;
@@ -1169,8 +1169,9 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
const SERVER_PATH: &str =
"node_modules/@github/copilot-language-server/dist/language-server.js";
// pinning it: https://github.com/zed-industries/zed/issues/36093
const PINNED_VERSION: &str = "1.354";
let latest_version = node_runtime
.npm_package_latest_version(PACKAGE_NAME)
.await?;
let server_path = paths::copilot_dir().join(SERVER_PATH);
fs.create_dir(paths::copilot_dir()).await?;
@@ -1180,13 +1181,12 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
PACKAGE_NAME,
&server_path,
paths::copilot_dir(),
&PINNED_VERSION,
VersionCheck::VersionMismatch,
&latest_version,
)
.await;
if should_install {
node_runtime
.npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &PINNED_VERSION)])
.npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)])
.await?;
}

View File

@@ -74,6 +74,12 @@ impl Borrow<str> for DebugAdapterName {
}
}
impl Borrow<SharedString> for DebugAdapterName {
fn borrow(&self) -> &SharedString {
&self.0
}
}
impl std::fmt::Display for DebugAdapterName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)

View File

@@ -87,7 +87,7 @@ impl DapRegistry {
self.0.read().adapters.get(name).cloned()
}
pub fn enumerate_adapters(&self) -> Vec<DebugAdapterName> {
pub fn enumerate_adapters<B: FromIterator<DebugAdapterName>>(&self) -> B {
self.0.read().adapters.keys().cloned().collect()
}
}

View File

@@ -152,9 +152,6 @@ impl PythonDebugAdapter {
maybe!(async move {
let response = latest_release.filter(|response| response.status().is_success())?;
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
std::fs::create_dir_all(&download_dir).ok()?;
let mut output = String::new();
response
.into_body()

View File

@@ -300,7 +300,7 @@ impl DebugPanel {
});
session.update(cx, |session, _| match &mut session.mode {
SessionState::Building(state_task) => {
SessionState::Booting(state_task) => {
*state_task = Some(boot_task);
}
SessionState::Running(_) => {

View File

@@ -299,59 +299,76 @@ pub fn init(cx: &mut App) {
else {
return;
};
let session = active_session
.read(cx)
.running_state
.read(cx)
.session()
.read(cx);
if session.is_terminated() {
return;
}
let editor = cx.entity().downgrade();
window.on_action(TypeId::of::<editor::actions::RunToCursor>(), {
let editor = editor.clone();
let active_session = active_session.clone();
move |_, phase, _, cx| {
if phase != DispatchPhase::Bubble {
return;
}
maybe!({
let (buffer, position, _) = editor
.update(cx, |editor, cx| {
let cursor_point: language::Point =
editor.selections.newest(cx).head();
editor
.buffer()
.read(cx)
.point_to_buffer_point(cursor_point, cx)
})
.ok()??;
window.on_action_when(
session.any_stopped_thread(),
TypeId::of::<editor::actions::RunToCursor>(),
{
let editor = editor.clone();
let active_session = active_session.clone();
move |_, phase, _, cx| {
if phase != DispatchPhase::Bubble {
return;
}
maybe!({
let (buffer, position, _) = editor
.update(cx, |editor, cx| {
let cursor_point: language::Point =
editor.selections.newest(cx).head();
let path =
editor
.buffer()
.read(cx)
.point_to_buffer_point(cursor_point, cx)
})
.ok()??;
let path =
debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
&buffer, cx,
)?;
let source_breakpoint = SourceBreakpoint {
row: position.row,
path,
message: None,
condition: None,
hit_condition: None,
state: debugger::breakpoint_store::BreakpointState::Enabled,
};
let source_breakpoint = SourceBreakpoint {
row: position.row,
path,
message: None,
condition: None,
hit_condition: None,
state: debugger::breakpoint_store::BreakpointState::Enabled,
};
active_session.update(cx, |session, cx| {
session.running_state().update(cx, |state, cx| {
if let Some(thread_id) = state.selected_thread_id() {
state.session().update(cx, |session, cx| {
session.run_to_position(
source_breakpoint,
thread_id,
cx,
);
})
}
active_session.update(cx, |session, cx| {
session.running_state().update(cx, |state, cx| {
if let Some(thread_id) = state.selected_thread_id() {
state.session().update(cx, |session, cx| {
session.run_to_position(
source_breakpoint,
thread_id,
cx,
);
})
}
});
});
});
Some(())
});
}
});
Some(())
});
}
},
);
window.on_action(
TypeId::of::<editor::actions::EvaluateSelectedText>(),

View File

@@ -1,5 +1,5 @@
use anyhow::{Context as _, bail};
use collections::{FxHashMap, HashMap};
use collections::{FxHashMap, HashMap, HashSet};
use language::LanguageRegistry;
use std::{
borrow::Cow,
@@ -450,7 +450,7 @@ impl NewProcessModal {
.and_then(|buffer| buffer.read(cx).language())
.cloned();
let mut available_adapters = workspace
let mut available_adapters: Vec<_> = workspace
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
.unwrap_or_default();
if let Some(language) = active_buffer_language {
@@ -1054,6 +1054,9 @@ impl DebugDelegate {
})
})
});
let valid_adapters: HashSet<_> = cx.global::<DapRegistry>().enumerate_adapters();
cx.spawn(async move |this, cx| {
let (recent, scenarios) = if let Some(task) = task {
task.await
@@ -1094,6 +1097,7 @@ impl DebugDelegate {
} => !(hide_vscode && dir.ends_with(".vscode")),
_ => true,
})
.filter(|(_, scenario)| valid_adapters.contains(&scenario.adapter))
.map(|(kind, scenario)| {
let (language, scenario) =
Self::get_scenario_kind(&languages, &dap_registry, scenario);

View File

@@ -1651,7 +1651,7 @@ impl RunningState {
let is_building = self.session.update(cx, |session, cx| {
session.shutdown(cx).detach();
matches!(session.mode, session::SessionState::Building(_))
matches!(session.mode, session::SessionState::Booting(_))
});
if is_building {

View File

@@ -29,7 +29,6 @@ use ui::{
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable,
Tooltip, Window, div, h_flex, px, v_flex,
};
use util::ResultExt;
use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
@@ -56,8 +55,6 @@ pub(crate) struct BreakpointList {
scrollbar_state: ScrollbarState,
breakpoints: Vec<BreakpointEntry>,
session: Option<Entity<Session>>,
hide_scrollbar_task: Option<Task<()>>,
show_scrollbar: bool,
focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
@@ -103,8 +100,6 @@ impl BreakpointList {
worktree_store,
scrollbar_state,
breakpoints: Default::default(),
hide_scrollbar_task: None,
show_scrollbar: false,
workspace,
session,
focus_handle,
@@ -565,21 +560,6 @@ impl BreakpointList {
Ok(())
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(cx, |panel, cx| {
panel.show_scrollbar = false;
cx.notify();
})
.log_err();
}))
}
fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let selected_ix = self.selected_ix;
let focus_handle = self.focus_handle.clone();
@@ -614,43 +594,39 @@ impl BreakpointList {
.flex_grow()
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !(self.show_scrollbar || self.scrollbar_state.is_dragging()) {
return None;
}
Some(
div()
.occlude()
.id("breakpoint-list-vertical-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.occlude()
.id("breakpoint-list-vertical-scrollbar")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
)
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
}
pub(crate) fn render_control_strip(&self) -> AnyElement {
let selection_kind = self.selection_kind();
let focus_handle = self.focus_handle.clone();
@@ -819,15 +795,6 @@ impl Render for BreakpointList {
.id("breakpoint-list")
.key_context("BreakpointList")
.track_focus(&self.focus_handle)
.on_hover(cx.listener(|this, hovered, window, cx| {
if *hovered {
this.show_scrollbar = true;
this.hide_scrollbar_task.take();
cx.notify();
} else if !this.focus_handle.contains_focused(window, cx) {
this.hide_scrollbar(window, cx);
}
}))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
@@ -844,7 +811,7 @@ impl Render for BreakpointList {
v_flex()
.size_full()
.child(self.render_list(cx))
.children(self.render_vertical_scrollbar(cx)),
.child(self.render_vertical_scrollbar(cx)),
)
.when_some(self.strip_mode, |this, _| {
this.child(Divider::horizontal()).child(

View File

@@ -23,7 +23,6 @@ use ui::{
ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString,
StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex,
};
use util::ResultExt;
use workspace::Workspace;
use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList};
@@ -34,9 +33,7 @@ pub(crate) struct MemoryView {
workspace: WeakEntity<Workspace>,
scroll_handle: UniformListScrollHandle,
scroll_state: ScrollbarState,
show_scrollbar: bool,
stack_frame_list: WeakEntity<StackFrameList>,
hide_scrollbar_task: Option<Task<()>>,
focus_handle: FocusHandle,
view_state: ViewState,
query_editor: Entity<Editor>,
@@ -150,8 +147,6 @@ impl MemoryView {
scroll_state,
scroll_handle,
stack_frame_list,
show_scrollbar: false,
hide_scrollbar_task: None,
focus_handle: cx.focus_handle(),
view_state,
query_editor,
@@ -168,61 +163,42 @@ impl MemoryView {
.detach();
this
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(cx, |panel, cx| {
panel.show_scrollbar = false;
cx.notify();
})
.log_err();
}))
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !(self.show_scrollbar || self.scroll_state.is_dragging()) {
return None;
}
Some(
div()
.occlude()
.id("memory-view-vertical-scrollbar")
.on_drag_move(cx.listener(|this, evt, _, cx| {
let did_handle = this.handle_scroll_drag(evt);
cx.notify();
if did_handle {
cx.stop_propagation()
}
}))
.on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty))
.on_hover(|_, _, cx| {
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
div()
.occlude()
.id("memory-view-vertical-scrollbar")
.on_drag_move(cx.listener(|this, evt, _, cx| {
let did_handle = this.handle_scroll_drag(evt);
cx.notify();
if did_handle {
cx.stop_propagation()
}
}))
.on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scroll_state.clone())),
)
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scroll_state.clone()).map(|s| s.auto_hide(cx)))
}
fn render_memory(&self, cx: &mut Context<Self>) -> UniformList {
@@ -920,15 +896,6 @@ impl Render for MemoryView {
.on_action(cx.listener(Self::page_up))
.size_full()
.track_focus(&self.focus_handle)
.on_hover(cx.listener(|this, hovered, window, cx| {
if *hovered {
this.show_scrollbar = true;
this.hide_scrollbar_task.take();
cx.notify();
} else if !this.focus_handle.contains_focused(window, cx) {
this.hide_scrollbar(window, cx);
}
}))
.child(
h_flex()
.w_full()
@@ -978,7 +945,7 @@ impl Render for MemoryView {
)
.with_priority(1)
}))
.children(self.render_vertical_scrollbar(cx)),
.child(self.render_vertical_scrollbar(cx)),
)
}
}

View File

@@ -298,7 +298,7 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
let adapter_names = cx.update(|cx| {
let registry = DapRegistry::global(cx);
registry.enumerate_adapters()
registry.enumerate_adapters::<Vec<_>>()
});
let zed_config = ZedDebugConfig {

View File

@@ -8028,20 +8028,12 @@ impl Element for EditorElement {
autoscroll_containing_element,
needs_horizontal_autoscroll,
) = self.editor.update(cx, |editor, cx| {
let autoscroll_request = editor.scroll_manager.take_autoscroll_request();
let autoscroll_request = editor.autoscroll_request();
let autoscroll_containing_element =
autoscroll_request.is_some() || editor.has_pending_selection();
let (needs_horizontal_autoscroll, was_scrolled) = editor
.autoscroll_vertically(
bounds,
line_height,
max_scroll_top,
autoscroll_request,
window,
cx,
);
.autoscroll_vertically(bounds, line_height, max_scroll_top, window, cx);
if was_scrolled.0 {
snapshot = editor.snapshot(window, cx);
}
@@ -8431,11 +8423,7 @@ impl Element for EditorElement {
Ok(blocks) => blocks,
Err(resized_blocks) => {
self.editor.update(cx, |editor, cx| {
editor.resize_blocks(
resized_blocks,
autoscroll_request.map(|(autoscroll, _)| autoscroll),
cx,
)
editor.resize_blocks(resized_blocks, autoscroll_request, cx)
});
return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx);
}
@@ -8480,7 +8468,6 @@ impl Element for EditorElement {
scroll_width,
em_advance,
&line_layouts,
autoscroll_request,
window,
cx,
)

View File

@@ -1,8 +1,8 @@
use crate::{
Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor,
EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation,
GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionEffects,
SelectionExt, ToDisplayPoint, ToggleCodeActions,
GoToTypeDefinition, Paste, Rename, RevealInFileManager, RunToCursor, SelectMode,
SelectionEffects, SelectionExt, ToDisplayPoint, ToggleCodeActions,
actions::{Format, FormatSelections},
selections_collection::SelectionsCollection,
};
@@ -200,15 +200,21 @@ pub fn deploy_context_menu(
});
let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
let run_to_cursor = window.is_action_available(&RunToCursor, cx);
ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
let builder = menu
.on_blur_subscription(Subscription::new(|| {}))
.when(evaluate_selection && has_selections, |builder| {
builder
.action("Evaluate Selection", Box::new(EvaluateSelectedText))
.separator()
.when(run_to_cursor, |builder| {
builder.action("Run to Cursor", Box::new(RunToCursor))
})
.when(evaluate_selection && has_selections, |builder| {
builder.action("Evaluate Selection", Box::new(EvaluateSelectedText))
})
.when(
run_to_cursor || (evaluate_selection && has_selections),
|builder| builder.separator(),
)
.action("Go to Definition", Box::new(GoToDefinition))
.action("Go to Declaration", Box::new(GoToDeclaration))
.action("Go to Type Definition", Box::new(GoToTypeDefinition))

View File

@@ -348,8 +348,8 @@ impl ScrollManager {
self.show_scrollbars
}
pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> {
self.autoscroll_request.take()
pub fn autoscroll_request(&self) -> Option<Autoscroll> {
self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
}
pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> {

View File

@@ -102,12 +102,15 @@ impl AutoscrollStrategy {
pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool);
impl Editor {
pub fn autoscroll_request(&self) -> Option<Autoscroll> {
self.scroll_manager.autoscroll_request()
}
pub(crate) fn autoscroll_vertically(
&mut self,
bounds: Bounds<Pixels>,
line_height: Pixels,
max_scroll_top: f32,
autoscroll_request: Option<(Autoscroll, bool)>,
window: &mut Window,
cx: &mut Context<Editor>,
) -> (NeedsHorizontalAutoscroll, WasScrolled) {
@@ -134,7 +137,7 @@ impl Editor {
WasScrolled(false)
};
let Some((autoscroll, local)) = autoscroll_request else {
let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
return (NeedsHorizontalAutoscroll(false), editor_was_scrolled);
};
@@ -281,12 +284,9 @@ impl Editor {
scroll_width: Pixels,
em_advance: Pixels,
layouts: &[LineWithInvisibles],
autoscroll_request: Option<(Autoscroll, bool)>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<gpui::Point<f32>> {
let (_, local) = autoscroll_request?;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx);
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
@@ -335,10 +335,10 @@ impl Editor {
let was_scrolled = if target_left < scroll_left {
scroll_position.x = target_left / em_advance;
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
self.set_scroll_position_internal(scroll_position, true, true, window, cx)
} else if target_right > scroll_right {
scroll_position.x = (target_right - viewport_width) / em_advance;
self.set_scroll_position_internal(scroll_position, local, true, window, cx)
self.set_scroll_position_internal(scroll_position, true, true, window, cx)
} else {
WasScrolled(false)
};

View File

@@ -158,11 +158,6 @@ where
}
}
#[derive(Debug)]
pub struct OnFlagsReady {
pub is_staff: bool,
}
pub trait FeatureFlagAppExt {
fn wait_for_flag<T: FeatureFlag>(&mut self) -> WaitForFlag;
@@ -174,10 +169,6 @@ pub trait FeatureFlagAppExt {
fn has_flag<T: FeatureFlag>(&self) -> bool;
fn is_staff(&self) -> bool;
fn on_flags_ready<F>(&mut self, callback: F) -> Subscription
where
F: FnMut(OnFlagsReady, &mut App) + 'static;
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
where
F: FnMut(bool, &mut App) + 'static;
@@ -207,21 +198,6 @@ impl FeatureFlagAppExt for App {
.unwrap_or(false)
}
fn on_flags_ready<F>(&mut self, mut callback: F) -> Subscription
where
F: FnMut(OnFlagsReady, &mut App) + 'static,
{
self.observe_global::<FeatureFlags>(move |cx| {
let feature_flags = cx.global::<FeatureFlags>();
callback(
OnFlagsReady {
is_staff: feature_flags.staff,
},
cx,
);
})
}
fn observe_flag<T: FeatureFlag, F>(&mut self, mut callback: F) -> Subscription
where
F: FnMut(bool, &mut App) + 'static,

View File

@@ -846,14 +846,12 @@ impl GitRepository for RealGitRepository {
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
child
.stdin
.take()
.unwrap()
.write_all(content.as_bytes())
.await?;
let mut stdin = child.stdin.take().unwrap();
stdin.write_all(content.as_bytes()).await?;
stdin.flush().await?;
drop(stdin);
let output = child.output().await?.stdout;
let sha = String::from_utf8(output)?;
let sha = str::from_utf8(&output)?.trim();
log::debug!("indexing SHA: {sha}, path {path:?}");
@@ -871,6 +869,7 @@ impl GitRepository for RealGitRepository {
String::from_utf8_lossy(&output.stderr)
);
} else {
log::debug!("removing path {path:?} from the index");
let output = new_smol_command(&git_binary_path)
.current_dir(&working_directory)
.envs(env.iter())
@@ -921,6 +920,7 @@ impl GitRepository for RealGitRepository {
for rev in &revs {
write!(&mut stdin, "{rev}\n")?;
}
stdin.flush()?;
drop(stdin);
let output = process.wait_with_output()?;

View File

@@ -180,6 +180,7 @@ impl Focusable for BranchList {
impl Render for BranchList {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("GitBranchSelector")
.w(self.width)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.child(self.picker.clone())

View File

@@ -109,7 +109,10 @@ impl Focusable for RepositorySelector {
impl Render for RepositorySelector {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().w(self.width).child(self.picker.clone())
div()
.key_context("GitRepositorySelector")
.w(self.width)
.child(self.picker.clone())
}
}

View File

@@ -150,7 +150,7 @@ pub struct MouseClickEvent {
}
/// A click event that was generated by a keyboard button being pressed and released.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct KeyboardClickEvent {
/// The keyboard button that was pressed to trigger the click.
pub button: KeyboardButton,
@@ -168,6 +168,12 @@ pub enum ClickEvent {
Keyboard(KeyboardClickEvent),
}
impl Default for ClickEvent {
fn default() -> Self {
ClickEvent::Keyboard(KeyboardClickEvent::default())
}
}
impl ClickEvent {
/// Returns the modifiers that were held during the click event
///
@@ -256,9 +262,10 @@ impl ClickEvent {
}
/// An enum representing the keyboard button that was pressed for a click event.
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug, Default)]
pub enum KeyboardButton {
/// Enter key was clicked
#[default]
Enter,
/// Space key was clicked
Space,

View File

@@ -4248,6 +4248,25 @@ impl Window {
.on_action(action_type, Rc::new(listener));
}
/// Register an action listener on the window for the next frame if the condition is true.
/// The type of action is determined by the first parameter of the given listener.
/// When the next frame is rendered the listener will be cleared.
///
/// This is a fairly low-level method, so prefer using action handlers on elements unless you have
/// a specific need to register a global listener.
pub fn on_action_when(
&mut self,
condition: bool,
action_type: TypeId,
listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static,
) {
if condition {
self.next_frame
.dispatch_tree
.on_action(action_type, Rc::new(listener));
}
}
/// Read information about the GPU backing this window.
/// Currently returns None on Mac and Windows.
pub fn gpu_specs(&self) -> Option<GpuSpecs> {

View File

@@ -71,19 +71,11 @@ pub async fn latest_github_release(
}
};
let mut release = releases
releases
.into_iter()
.filter(|release| !require_assets || !release.assets.is_empty())
.find(|release| release.pre_release == pre_release)
.context("finding a prerelease")?;
release.assets.iter_mut().for_each(|asset| {
if let Some(digest) = &mut asset.digest {
if let Some(stripped) = digest.strip_prefix("sha256:") {
*digest = stripped.to_owned();
}
}
});
Ok(release)
.context("finding a prerelease")
}
pub async fn get_release_by_tag_name(

View File

@@ -261,7 +261,6 @@ pub enum IconName {
TodoComplete,
TodoPending,
TodoProgress,
ToolBulb,
ToolCopy,
ToolDeleteFile,
ToolDiagnostics,
@@ -273,6 +272,7 @@ pub enum IconName {
ToolRegex,
ToolSearch,
ToolTerminal,
ToolThink,
ToolWeb,
Trash,
Triangle,

View File

@@ -20,7 +20,6 @@ anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
base64.workspace = true
client.workspace = true
cloud_api_types.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
futures.workspace = true

View File

@@ -3,9 +3,11 @@ use std::sync::Arc;
use anyhow::Result;
use client::Client;
use cloud_api_types::websocket_protocol::MessageToClient;
use cloud_llm_client::Plan;
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _};
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, ReadGlobal as _,
};
use proto::TypedEnvelope;
use smol::lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard};
use thiserror::Error;
@@ -80,7 +82,9 @@ impl Global for GlobalRefreshLlmTokenListener {}
pub struct RefreshLlmTokenEvent;
pub struct RefreshLlmTokenListener;
pub struct RefreshLlmTokenListener {
_llm_token_subscription: client::Subscription,
}
impl EventEmitter<RefreshLlmTokenEvent> for RefreshLlmTokenListener {}
@@ -95,21 +99,17 @@ impl RefreshLlmTokenListener {
}
fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
client.add_message_to_client_handler({
let this = cx.entity();
move |message, cx| {
Self::handle_refresh_llm_token(this.clone(), message, cx);
}
});
Self
}
fn handle_refresh_llm_token(this: Entity<Self>, message: &MessageToClient, cx: &mut App) {
match message {
MessageToClient::UserUpdated => {
this.update(cx, |_this, cx| cx.emit(RefreshLlmTokenEvent));
}
Self {
_llm_token_subscription: client
.add_message_handler(cx.weak_entity(), Self::handle_refresh_llm_token),
}
}
async fn handle_refresh_llm_token(
this: Entity<Self>,
_: TypedEnvelope<proto::RefreshLlmToken>,
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |_this, cx| cx.emit(RefreshLlmTokenEvent))
}
}

View File

@@ -941,8 +941,6 @@ impl LanguageModel for CloudLanguageModel {
request,
model.id(),
model.supports_parallel_tool_calls(),
model.supports_prompt_cache_key(),
None,
None,
);
let llm_api_token = self.llm_api_token.clone();

View File

@@ -14,7 +14,7 @@ use language_model::{
RateLimiter, Role, StopReason, TokenUsage,
};
use menu;
use open_ai::{ImageUrl, Model, ReasoningEffort, ResponseStreamEvent, stream_completion};
use open_ai::{ImageUrl, Model, ResponseStreamEvent, stream_completion};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
@@ -45,7 +45,6 @@ pub struct AvailableModel {
pub max_tokens: u64,
pub max_output_tokens: Option<u64>,
pub max_completion_tokens: Option<u64>,
pub reasoning_effort: Option<ReasoningEffort>,
}
pub struct OpenAiLanguageModelProvider {
@@ -214,7 +213,6 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
max_tokens: model.max_tokens,
max_output_tokens: model.max_output_tokens,
max_completion_tokens: model.max_completion_tokens,
reasoning_effort: model.reasoning_effort.clone(),
},
);
}
@@ -303,25 +301,7 @@ impl LanguageModel for OpenAiLanguageModel {
}
fn supports_images(&self) -> bool {
use open_ai::Model;
match &self.model {
Model::FourOmni
| Model::FourOmniMini
| Model::FourPointOne
| Model::FourPointOneMini
| Model::FourPointOneNano
| Model::Five
| Model::FiveMini
| Model::FiveNano
| Model::O1
| Model::O3
| Model::O4Mini => true,
Model::ThreePointFiveTurbo
| Model::Four
| Model::FourTurbo
| Model::O3Mini
| Model::Custom { .. } => false,
}
false
}
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
@@ -370,9 +350,7 @@ impl LanguageModel for OpenAiLanguageModel {
request,
self.model.id(),
self.model.supports_parallel_tool_calls(),
self.model.supports_prompt_cache_key(),
self.max_output_tokens(),
self.model.reasoning_effort(),
);
let completions = self.stream_completion(request, cx);
async move {
@@ -387,9 +365,7 @@ pub fn into_open_ai(
request: LanguageModelRequest,
model_id: &str,
supports_parallel_tool_calls: bool,
supports_prompt_cache_key: bool,
max_output_tokens: Option<u64>,
reasoning_effort: Option<ReasoningEffort>,
) -> open_ai::Request {
let stream = !model_id.starts_with("o1-");
@@ -479,11 +455,6 @@ pub fn into_open_ai(
} else {
None
},
prompt_cache_key: if supports_prompt_cache_key {
request.thread_id
} else {
None
},
tools: request
.tools
.into_iter()
@@ -500,7 +471,6 @@ pub fn into_open_ai(
LanguageModelToolChoice::Any => open_ai::ToolChoice::Required,
LanguageModelToolChoice::None => open_ai::ToolChoice::None,
}),
reasoning_effort,
}
}
@@ -704,10 +674,6 @@ pub fn count_open_ai_tokens(
| Model::O3
| Model::O3Mini
| Model::O4Mini => tiktoken_rs::num_tokens_from_messages(model.id(), &messages),
// GPT-5 models don't have tiktoken support yet; fall back on gpt-4o tokenizer
Model::Five | Model::FiveMini | Model::FiveNano => {
tiktoken_rs::num_tokens_from_messages("gpt-4o", &messages)
}
}
.map(|tokens| tokens as u64)
})

View File

@@ -355,16 +355,7 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
LanguageModelCompletionError,
>,
> {
let supports_parallel_tool_call = true;
let supports_prompt_cache_key = false;
let request = into_open_ai(
request,
&self.model.name,
supports_parallel_tool_call,
supports_prompt_cache_key,
self.max_output_tokens(),
None,
);
let request = into_open_ai(request, &self.model.name, true, self.max_output_tokens());
let completions = self.stream_completion(request, cx);
async move {
let mapper = OpenAiEventMapper::new();

View File

@@ -355,9 +355,7 @@ impl LanguageModel for VercelLanguageModel {
request,
self.model.id(),
self.model.supports_parallel_tool_calls(),
self.model.supports_prompt_cache_key(),
self.max_output_tokens(),
None,
);
let completions = self.stream_completion(request, cx);
async move {

View File

@@ -359,9 +359,7 @@ impl LanguageModel for XAiLanguageModel {
request,
self.model.id(),
self.model.supports_parallel_tool_calls(),
self.model.supports_prompt_cache_key(),
self.max_output_tokens(),
None,
);
let completions = self.stream_completion(request, cx);
async move {

View File

@@ -86,7 +86,10 @@ impl LanguageSelector {
impl Render for LanguageSelector {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
v_flex()
.key_context("LanguageSelector")
.w(rems(34.))
.child(self.picker.clone())
}
}

View File

@@ -71,11 +71,8 @@ impl super::LspAdapter for CLspAdapter {
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let GitHubLspBinaryVersion {
name,
url,
digest: expected_digest,
} = *version.downcast::<GitHubLspBinaryVersion>().unwrap();
let GitHubLspBinaryVersion { name, url, digest } =
&*version.downcast::<GitHubLspBinaryVersion>().unwrap();
let version_dir = container_dir.join(format!("clangd_{name}"));
let binary_path = version_dir.join("bin/clangd");
@@ -102,9 +99,7 @@ impl super::LspAdapter for CLspAdapter {
log::warn!("Unable to run {binary_path:?} asset, redownloading: {err}",)
})
};
if let (Some(actual_digest), Some(expected_digest)) =
(&metadata.digest, &expected_digest)
{
if let (Some(actual_digest), Some(expected_digest)) = (&metadata.digest, digest) {
if actual_digest == expected_digest {
if validity_check().await.is_ok() {
return Ok(binary);
@@ -120,8 +115,8 @@ impl super::LspAdapter for CLspAdapter {
}
download_server_binary(
delegate,
&url,
expected_digest.as_deref(),
url,
digest.as_deref(),
&container_dir,
AssetKind::Zip,
)
@@ -130,7 +125,7 @@ impl super::LspAdapter for CLspAdapter {
GithubBinaryMetadata::write_to_file(
&GithubBinaryMetadata {
metadata_version: 1,
digest: expected_digest,
digest: digest.clone(),
},
&metadata_path,
)

View File

@@ -103,13 +103,7 @@ impl LspAdapter for CssLspAdapter {
let should_install_language_server = self
.node
.should_install_npm_package(
Self::PACKAGE_NAME,
&server_path,
&container_dir,
&version,
Default::default(),
)
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
.await;
if should_install_language_server {

View File

@@ -18,8 +18,9 @@ impl GithubBinaryMetadata {
let metadata_content = async_fs::read_to_string(metadata_path)
.await
.with_context(|| format!("reading metadata file at {metadata_path:?}"))?;
serde_json::from_str(&metadata_content)
.with_context(|| format!("parsing metadata file at {metadata_path:?}"))
let metadata: GithubBinaryMetadata = serde_json::from_str(&metadata_content)
.with_context(|| format!("parsing metadata file at {metadata_path:?}"))?;
Ok(metadata)
}
pub(crate) async fn write_to_file(&self, metadata_path: &Path) -> Result<()> {
@@ -61,7 +62,6 @@ pub(crate) async fn download_server_binary(
format!("saving archive contents into the temporary file for {url}",)
})?;
let asset_sha_256 = format!("{:x}", writer.hasher.finalize());
anyhow::ensure!(
asset_sha_256 == expected_sha_256,
"{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}",

View File

@@ -340,13 +340,7 @@ impl LspAdapter for JsonLspAdapter {
let should_install_language_server = self
.node
.should_install_npm_package(
Self::PACKAGE_NAME,
&server_path,
&container_dir,
&version,
Default::default(),
)
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
.await;
if should_install_language_server {

View File

@@ -206,7 +206,6 @@ impl LspAdapter for PythonLspAdapter {
&server_path,
&container_dir,
&version,
Default::default(),
)
.await;

View File

@@ -22,7 +22,7 @@ use std::{
sync::{Arc, LazyLock},
};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::fs::{make_file_executable, remove_matching};
use util::fs::make_file_executable;
use util::merge_json_value_into;
use util::{ResultExt, maybe};
@@ -161,13 +161,13 @@ impl LspAdapter for RustLspAdapter {
let asset_name = Self::build_asset_name();
let asset = release
.assets
.into_iter()
.iter()
.find(|asset| asset.name == asset_name)
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
Ok(Box::new(GitHubLspBinaryVersion {
name: release.tag_name,
url: asset.browser_download_url,
digest: asset.digest,
url: asset.browser_download_url.clone(),
digest: asset.digest.clone(),
}))
}
@@ -177,11 +177,11 @@ impl LspAdapter for RustLspAdapter {
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let GitHubLspBinaryVersion {
name,
url,
digest: expected_digest,
} = *version.downcast::<GitHubLspBinaryVersion>().unwrap();
let GitHubLspBinaryVersion { name, url, digest } =
&*version.downcast::<GitHubLspBinaryVersion>().unwrap();
let expected_digest = digest
.as_ref()
.and_then(|digest| digest.strip_prefix("sha256:"));
let destination_path = container_dir.join(format!("rust-analyzer-{name}"));
let server_path = match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
@@ -212,7 +212,7 @@ impl LspAdapter for RustLspAdapter {
})
};
if let (Some(actual_digest), Some(expected_digest)) =
(&metadata.digest, &expected_digest)
(&metadata.digest, expected_digest)
{
if actual_digest == expected_digest {
if validity_check().await.is_ok() {
@@ -228,20 +228,20 @@ impl LspAdapter for RustLspAdapter {
}
}
_ = fs::remove_dir_all(&destination_path).await;
download_server_binary(
delegate,
&url,
expected_digest.as_deref(),
url,
expected_digest,
&destination_path,
Self::GITHUB_ASSET_KIND,
)
.await?;
make_file_executable(&server_path).await?;
remove_matching(&container_dir, |path| server_path != path).await;
GithubBinaryMetadata::write_to_file(
&GithubBinaryMetadata {
metadata_version: 1,
digest: expected_digest,
digest: expected_digest.map(ToString::to_string),
},
&metadata_path,
)

View File

@@ -108,13 +108,7 @@ impl LspAdapter for TailwindLspAdapter {
let should_install_language_server = self
.node
.should_install_npm_package(
Self::PACKAGE_NAME,
&server_path,
&container_dir,
&version,
Default::default(),
)
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
.await;
if should_install_language_server {

View File

@@ -589,7 +589,6 @@ impl LspAdapter for TypeScriptLspAdapter {
&server_path,
&container_dir,
version.typescript_version.as_str(),
Default::default(),
)
.await;

View File

@@ -116,7 +116,6 @@ impl LspAdapter for VtslsLspAdapter {
&server_path,
&container_dir,
&latest_version.server_version,
Default::default(),
)
.await
{
@@ -130,7 +129,6 @@ impl LspAdapter for VtslsLspAdapter {
&container_dir.join(Self::TYPESCRIPT_TSDK_PATH),
&container_dir,
&latest_version.typescript_version,
Default::default(),
)
.await
{

View File

@@ -104,13 +104,7 @@ impl LspAdapter for YamlLspAdapter {
let should_install_language_server = self
.node
.should_install_npm_package(
Self::PACKAGE_NAME,
&server_path,
&container_dir,
&version,
Default::default(),
)
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
.await;
if should_install_language_server {

View File

@@ -1,6 +1,6 @@
name = "YAML"
grammar = "yaml"
path_suffixes = ["yml", "yaml"]
path_suffixes = ["yml", "yaml", "pixi.lock"]
line_comments = ["# "]
autoclose_before = ",]}"
brackets = [

View File

@@ -747,6 +747,10 @@ impl LanguageServer {
InsertTextMode::ADJUST_INDENTATION,
],
}),
documentation_format: Some(vec![
MarkupKind::Markdown,
MarkupKind::PlainText,
]),
..Default::default()
}),
insert_text_mode: Some(InsertTextMode::ADJUST_INDENTATION),

View File

@@ -29,15 +29,6 @@ pub struct NodeBinaryOptions {
pub use_paths: Option<(PathBuf, PathBuf)>,
}
#[derive(Default)]
pub enum VersionCheck {
/// Check whether the installed and requested version have a mismatch
VersionMismatch,
/// Only check whether the currently installed version is older than the newest one
#[default]
OlderVersion,
}
#[derive(Clone)]
pub struct NodeRuntime(Arc<Mutex<NodeRuntimeState>>);
@@ -296,7 +287,6 @@ impl NodeRuntime {
local_executable_path: &Path,
local_package_directory: &Path,
latest_version: &str,
version_check: VersionCheck,
) -> bool {
// In the case of the local system not having the package installed,
// or in the instances where we fail to parse package.json data,
@@ -321,10 +311,7 @@ impl NodeRuntime {
return true;
};
match version_check {
VersionCheck::VersionMismatch => installed_version != latest_version,
VersionCheck::OlderVersion => installed_version < latest_version,
}
installed_version < latest_version
}
}

View File

@@ -58,7 +58,7 @@ fn get_max_tokens(name: &str) -> u64 {
"magistral" => 40000,
"llama3.1" | "llama3.2" | "llama3.3" | "phi3" | "phi3.5" | "phi4" | "command-r"
| "qwen3" | "gemma3" | "deepseek-coder-v2" | "deepseek-v3" | "deepseek-r1" | "yi-coder"
| "devstral" => 128000,
| "devstral" | "gpt-oss" => 128000,
_ => DEFAULT_TOKENS,
}
.clamp(1, MAXIMUM_TOKENS)

View File

@@ -20,7 +20,6 @@ anyhow.workspace = true
futures.workspace = true
http_client.workspace = true
schemars = { workspace = true, optional = true }
log.workspace = true
serde.workspace = true
serde_json.workspace = true
strum.workspace = true

View File

@@ -74,12 +74,6 @@ pub enum Model {
O3,
#[serde(rename = "o4-mini")]
O4Mini,
#[serde(rename = "gpt-5")]
Five,
#[serde(rename = "gpt-5-mini")]
FiveMini,
#[serde(rename = "gpt-5-nano")]
FiveNano,
#[serde(rename = "custom")]
Custom {
@@ -89,13 +83,11 @@ pub enum Model {
max_tokens: u64,
max_output_tokens: Option<u64>,
max_completion_tokens: Option<u64>,
reasoning_effort: Option<ReasoningEffort>,
},
}
impl Model {
pub fn default_fast() -> Self {
// TODO: Replace with FiveMini since all other models are deprecated
Self::FourPointOneMini
}
@@ -113,9 +105,6 @@ impl Model {
"o3-mini" => Ok(Self::O3Mini),
"o3" => Ok(Self::O3),
"o4-mini" => Ok(Self::O4Mini),
"gpt-5" => Ok(Self::Five),
"gpt-5-mini" => Ok(Self::FiveMini),
"gpt-5-nano" => Ok(Self::FiveNano),
invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"),
}
}
@@ -134,9 +123,6 @@ impl Model {
Self::O3Mini => "o3-mini",
Self::O3 => "o3",
Self::O4Mini => "o4-mini",
Self::Five => "gpt-5",
Self::FiveMini => "gpt-5-mini",
Self::FiveNano => "gpt-5-nano",
Self::Custom { name, .. } => name,
}
}
@@ -155,9 +141,6 @@ impl Model {
Self::O3Mini => "o3-mini",
Self::O3 => "o3",
Self::O4Mini => "o4-mini",
Self::Five => "gpt-5",
Self::FiveMini => "gpt-5-mini",
Self::FiveNano => "gpt-5-nano",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
@@ -178,9 +161,6 @@ impl Model {
Self::O3Mini => 200_000,
Self::O3 => 200_000,
Self::O4Mini => 200_000,
Self::Five => 272_000,
Self::FiveMini => 272_000,
Self::FiveNano => 272_000,
Self::Custom { max_tokens, .. } => *max_tokens,
}
}
@@ -202,18 +182,6 @@ impl Model {
Self::O3Mini => Some(100_000),
Self::O3 => Some(100_000),
Self::O4Mini => Some(100_000),
Self::Five => Some(128_000),
Self::FiveMini => Some(128_000),
Self::FiveNano => Some(128_000),
}
}
pub fn reasoning_effort(&self) -> Option<ReasoningEffort> {
match self {
Self::Custom {
reasoning_effort, ..
} => reasoning_effort.to_owned(),
_ => None,
}
}
@@ -229,20 +197,10 @@ impl Model {
| Self::FourOmniMini
| Self::FourPointOne
| Self::FourPointOneMini
| Self::FourPointOneNano
| Self::Five
| Self::FiveMini
| Self::FiveNano => true,
| Self::FourPointOneNano => true,
Self::O1 | Self::O3 | Self::O3Mini | Self::O4Mini | Model::Custom { .. } => false,
}
}
/// Returns whether the given model supports the `prompt_cache_key` parameter.
///
/// If the model does not support the parameter, do not pass it up.
pub fn supports_prompt_cache_key(&self) -> bool {
return true;
}
}
#[derive(Debug, Serialize, Deserialize)]
@@ -262,10 +220,6 @@ pub struct Request {
pub parallel_tool_calls: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<ToolDefinition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffort>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -277,16 +231,6 @@ pub enum ToolChoice {
Other(ToolDefinition),
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "lowercase")]
pub enum ReasoningEffort {
Minimal,
Low,
Medium,
High,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolDefinition {
@@ -477,15 +421,7 @@ pub async fn stream_completion(
Ok(ResponseStreamResult::Err { error }) => {
Some(Err(anyhow!(error)))
}
Err(error) => {
log::error!(
"Failed to parse OpenAI response into ResponseStreamResult: `{}`\n\
Response: `{}`",
error,
line,
);
Some(Err(anyhow!(error)))
}
Err(error) => Some(Err(anyhow!(error))),
}
}
}

View File

@@ -56,7 +56,7 @@ use std::{
};
use task::TaskContext;
use text::{PointUtf16, ToPointUtf16};
use util::{ResultExt, maybe};
use util::{ResultExt, debug_panic, maybe};
use worktree::Worktree;
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
@@ -141,7 +141,10 @@ pub struct DataBreakpointState {
}
pub enum SessionState {
Building(Option<Task<Result<()>>>),
/// Represents a session that is building/initializing
/// even if a session doesn't have a pre build task this state
/// is used to run all the async tasks that are required to start the session
Booting(Option<Task<Result<()>>>),
Running(RunningMode),
}
@@ -574,7 +577,7 @@ impl SessionState {
{
match self {
SessionState::Running(debug_adapter_client) => debug_adapter_client.request(request),
SessionState::Building(_) => Task::ready(Err(anyhow!(
SessionState::Booting(_) => Task::ready(Err(anyhow!(
"no adapter running to send request: {request:?}"
))),
}
@@ -583,7 +586,7 @@ impl SessionState {
/// Did this debug session stop at least once?
pub(crate) fn has_ever_stopped(&self) -> bool {
match self {
SessionState::Building(_) => false,
SessionState::Booting(_) => false,
SessionState::Running(running_mode) => running_mode.has_ever_stopped,
}
}
@@ -839,7 +842,7 @@ impl Session {
.detach();
let this = Self {
mode: SessionState::Building(None),
mode: SessionState::Booting(None),
id: session_id,
child_session_ids: HashSet::default(),
parent_session,
@@ -879,7 +882,7 @@ impl Session {
pub fn worktree(&self) -> Option<Entity<Worktree>> {
match &self.mode {
SessionState::Building(_) => None,
SessionState::Booting(_) => None,
SessionState::Running(local_mode) => local_mode.worktree.upgrade(),
}
}
@@ -940,14 +943,12 @@ impl Session {
.await?;
this.update(cx, |this, cx| {
match &mut this.mode {
SessionState::Building(task) if task.is_some() => {
SessionState::Booting(task) if task.is_some() => {
task.take().unwrap().detach_and_log_err(cx);
}
_ => {
debug_assert!(
this.parent_session.is_some(),
"Booting a root debug session without a boot task"
);
SessionState::Booting(_) => {}
SessionState::Running(_) => {
debug_panic!("Attempting to boot a session that is already running");
}
};
this.mode = SessionState::Running(mode);
@@ -1043,7 +1044,7 @@ impl Session {
pub fn binary(&self) -> Option<&DebugAdapterBinary> {
match &self.mode {
SessionState::Building(_) => None,
SessionState::Booting(_) => None,
SessionState::Running(running_mode) => Some(&running_mode.binary),
}
}
@@ -1089,26 +1090,26 @@ impl Session {
pub fn is_started(&self) -> bool {
match &self.mode {
SessionState::Building(_) => false,
SessionState::Booting(_) => false,
SessionState::Running(running) => running.is_started,
}
}
pub fn is_building(&self) -> bool {
matches!(self.mode, SessionState::Building(_))
matches!(self.mode, SessionState::Booting(_))
}
pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> {
match &mut self.mode {
SessionState::Running(local_mode) => Some(local_mode),
SessionState::Building(_) => None,
SessionState::Booting(_) => None,
}
}
pub fn as_running(&self) -> Option<&RunningMode> {
match &self.mode {
SessionState::Running(local_mode) => Some(local_mode),
SessionState::Building(_) => None,
SessionState::Booting(_) => None,
}
}
@@ -1302,7 +1303,7 @@ impl Session {
SessionState::Running(local_mode) => {
local_mode.initialize_sequence(&self.capabilities, initialize_rx, dap_store, cx)
}
SessionState::Building(_) => {
SessionState::Booting(_) => {
Task::ready(Err(anyhow!("cannot initialize, still building")))
}
}
@@ -1339,7 +1340,7 @@ impl Session {
})
.detach();
}
SessionState::Building(_) => {}
SessionState::Booting(_) => {}
}
}
@@ -2145,7 +2146,7 @@ impl Session {
)
}
}
SessionState::Building(build_task) => {
SessionState::Booting(build_task) => {
build_task.take();
Task::ready(Some(()))
}
@@ -2199,7 +2200,7 @@ impl Session {
pub fn adapter_client(&self) -> Option<Arc<DebugAdapterClient>> {
match self.mode {
SessionState::Running(ref local) => Some(local.client.clone()),
SessionState::Building(_) => None,
SessionState::Booting(_) => None,
}
}

View File

@@ -7722,19 +7722,12 @@ impl LspStore {
pub(crate) fn set_language_server_statuses_from_proto(
&mut self,
language_servers: Vec<proto::LanguageServer>,
server_capabilities: Vec<String>,
) {
self.language_server_statuses = language_servers
.into_iter()
.zip(server_capabilities)
.map(|(server, server_capabilities)| {
let server_id = LanguageServerId(server.id as usize);
if let Ok(server_capabilities) = serde_json::from_str(&server_capabilities) {
self.lsp_server_capabilities
.insert(server_id, server_capabilities);
}
.map(|server| {
(
server_id,
LanguageServerId(server.id as usize),
LanguageServerStatus {
name: LanguageServerName::from_proto(server.name),
pending_work: Default::default(),

View File

@@ -1488,10 +1488,7 @@ impl Project {
fs.clone(),
cx,
);
lsp_store.set_language_server_statuses_from_proto(
response.payload.language_servers,
response.payload.language_server_capabilities,
);
lsp_store.set_language_server_statuses_from_proto(response.payload.language_servers);
lsp_store
})?;
@@ -2322,10 +2319,7 @@ impl Project {
self.set_worktrees_from_proto(message.worktrees, cx)?;
self.set_collaborators_from_proto(message.collaborators, cx)?;
self.lsp_store.update(cx, |lsp_store, _| {
lsp_store.set_language_server_statuses_from_proto(
message.language_servers,
message.language_server_capabilities,
)
lsp_store.set_language_server_statuses_from_proto(message.language_servers)
});
self.enqueue_buffer_ordered_message(BufferOrderedMessage::Resync)
.unwrap();

View File

@@ -141,6 +141,7 @@ impl Focusable for RecentProjects {
impl Render for RecentProjects {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("RecentProjects")
.w(rems(self.rem_width))
.child(self.picker.clone())
.on_mouse_down_out(cx.listener(|this, _, window, cx| {

View File

@@ -374,6 +374,14 @@ impl Focusable for KeymapEditor {
}
}
}
/// Helper function to check if two keystroke sequences match exactly
fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool {
keystrokes1.len() == keystrokes2.len()
&& keystrokes1
.iter()
.zip(keystrokes2)
.all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers)
}
impl KeymapEditor {
fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
@@ -549,13 +557,7 @@ impl KeymapEditor {
.keystrokes()
.is_some_and(|keystrokes| {
if exact_match {
keystroke_query.len() == keystrokes.len()
&& keystroke_query.iter().zip(keystrokes).all(
|(query, keystroke)| {
query.key == keystroke.key
&& query.modifiers == keystroke.modifiers
},
)
keystrokes_match_exactly(&keystroke_query, keystrokes)
} else if keystroke_query.len() > keystrokes.len() {
return false;
} else {
@@ -2152,7 +2154,6 @@ impl KeybindingEditorModal {
let value = action_arguments
.as_ref()
.filter(|args| !args.is_empty())
.map(|args| {
serde_json::from_str(args).context("Failed to parse action arguments as JSON")
})
@@ -2341,8 +2342,50 @@ impl KeybindingEditorModal {
self.save_or_display_error(cx);
}
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent)
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
fn get_matching_bindings_count(&self, cx: &Context<Self>) -> usize {
let current_keystrokes = self.keybind_editor.read(cx).keystrokes().to_vec();
if current_keystrokes.is_empty() {
return 0;
}
self.keymap_editor
.read(cx)
.keybindings
.iter()
.enumerate()
.filter(|(idx, binding)| {
// Don't count the binding we're currently editing
if !self.creating && *idx == self.editing_keybind_idx {
return false;
}
binding
.keystrokes()
.map(|keystrokes| keystrokes_match_exactly(keystrokes, &current_keystrokes))
.unwrap_or(false)
})
.count()
}
fn show_matching_bindings(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
let keystrokes = self.keybind_editor.read(cx).keystrokes().to_vec();
// Dismiss the modal
cx.emit(DismissEvent);
// Update the keymap editor to show matching keystrokes
self.keymap_editor.update(cx, |editor, cx| {
editor.filter_state = FilterState::All;
editor.search_mode = SearchMode::KeyStroke { exact_match: true };
editor.keystroke_editor.update(cx, |keystroke_editor, cx| {
keystroke_editor.set_keystrokes(keystrokes, cx);
});
});
}
}
@@ -2357,6 +2400,7 @@ fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
impl Render for KeybindingEditorModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().colors();
let matching_bindings_count = self.get_matching_bindings_count(cx);
v_flex()
.w(rems(34.))
@@ -2428,19 +2472,37 @@ impl Render for KeybindingEditorModal {
),
)
.footer(
ModalFooter::new().end_slot(
h_flex()
.gap_1()
.child(
Button::new("cancel", "Cancel")
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
.child(Button::new("save-btn", "Save").on_click(cx.listener(
|this, _event, _window, cx| {
this.save_or_display_error(cx);
},
))),
),
ModalFooter::new()
.start_slot(
div().when(matching_bindings_count > 0, |this| {
this.child(
Button::new("show_matching", format!(
"There {} {} {} with the same keystrokes. Click to view",
if matching_bindings_count == 1 { "is" } else { "are" },
matching_bindings_count,
if matching_bindings_count == 1 { "binding" } else { "bindings" }
))
.style(ButtonStyle::Transparent)
.color(Color::Accent)
.on_click(cx.listener(|this, _, window, cx| {
this.show_matching_bindings(window, cx);
}))
)
})
)
.end_slot(
h_flex()
.gap_1()
.child(
Button::new("cancel", "Cancel")
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
.child(Button::new("save-btn", "Save").on_click(cx.listener(
|this, _event, _window, cx| {
this.save_or_display_error(cx);
},
))),
),
),
)
}

View File

@@ -40,7 +40,10 @@ impl IconThemeSelector {
impl Render for IconThemeSelector {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
v_flex()
.key_context("IconThemeSelector")
.w(rems(34.))
.child(self.picker.clone())
}
}

View File

@@ -92,7 +92,10 @@ impl Focusable for ThemeSelector {
impl Render for ThemeSelector {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
v_flex()
.key_context("ThemeSelector")
.w(rems(34.))
.child(self.picker.clone())
}
}

View File

@@ -95,7 +95,7 @@ impl RenderOnce for Disclosure {
impl Component for Disclosure {
fn scope() -> ComponentScope {
ComponentScope::Navigation
ComponentScope::Input
}
fn description() -> Option<&'static str> {

View File

@@ -1,11 +1,20 @@
use std::{any::Any, cell::Cell, fmt::Debug, ops::Range, rc::Rc, sync::Arc};
use std::{
any::Any,
cell::{Cell, RefCell},
fmt::Debug,
ops::Range,
rc::Rc,
sync::Arc,
time::Duration,
};
use crate::{IntoElement, prelude::*, px, relative};
use gpui::{
Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, CursorStyle,
Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
IsZero, LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Point, ScrollHandle, ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window, quad,
Point, ScrollHandle, ScrollWheelEvent, Size, Style, Task, UniformListScrollHandle, Window,
quad,
};
pub struct Scrollbar {
@@ -108,6 +117,25 @@ pub struct ScrollbarState {
thumb_state: Rc<Cell<ThumbState>>,
parent_id: Option<EntityId>,
scroll_handle: Arc<dyn ScrollableHandle>,
auto_hide: Rc<RefCell<AutoHide>>,
}
#[derive(Debug)]
enum AutoHide {
Disabled,
Hidden {
parent_id: EntityId,
},
Visible {
parent_id: EntityId,
_task: Task<()>,
},
}
impl AutoHide {
fn is_hidden(&self) -> bool {
matches!(self, AutoHide::Hidden { .. })
}
}
impl ScrollbarState {
@@ -116,6 +144,7 @@ impl ScrollbarState {
thumb_state: Default::default(),
parent_id: None,
scroll_handle: Arc::new(scroll),
auto_hide: Rc::new(RefCell::new(AutoHide::Disabled)),
}
}
@@ -174,6 +203,38 @@ impl ScrollbarState {
let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;
Some(thumb_percentage_start..thumb_percentage_end)
}
fn show_temporarily(&self, parent_id: EntityId, cx: &mut App) {
const SHOW_INTERVAL: Duration = Duration::from_secs(1);
let auto_hide = self.auto_hide.clone();
auto_hide.replace(AutoHide::Visible {
parent_id,
_task: cx.spawn({
let this = auto_hide.clone();
async move |cx| {
cx.background_executor().timer(SHOW_INTERVAL).await;
this.replace(AutoHide::Hidden { parent_id });
cx.update(|cx| {
cx.notify(parent_id);
})
.ok();
}
}),
});
}
fn unhide(&self, position: &Point<Pixels>, cx: &mut App) {
let parent_id = match &*self.auto_hide.borrow() {
AutoHide::Disabled => return,
AutoHide::Hidden { parent_id } => *parent_id,
AutoHide::Visible { parent_id, _task } => *parent_id,
};
if self.scroll_handle().viewport().contains(position) {
self.show_temporarily(parent_id, cx);
}
}
}
impl Scrollbar {
@@ -189,6 +250,14 @@ impl Scrollbar {
let thumb = state.thumb_range(kind)?;
Some(Self { thumb, state, kind })
}
/// Automatically hide the scrollbar when idle
pub fn auto_hide<V: 'static>(self, cx: &mut Context<V>) -> Self {
if matches!(*self.state.auto_hide.borrow(), AutoHide::Disabled) {
self.state.show_temporarily(cx.entity_id(), cx);
}
self
}
}
impl Element for Scrollbar {
@@ -284,16 +353,18 @@ impl Element for Scrollbar {
.apply_along(axis.invert(), |width| width / 1.5),
);
let corners = Corners::all(thumb_bounds.size.along(axis.invert()) / 2.0);
if thumb_state.is_dragging() || !self.state.auto_hide.borrow().is_hidden() {
let corners = Corners::all(thumb_bounds.size.along(axis.invert()) / 2.0);
window.paint_quad(quad(
thumb_bounds,
corners,
thumb_background,
Edges::default(),
Hsla::transparent_black(),
BorderStyle::default(),
));
window.paint_quad(quad(
thumb_bounds,
corners,
thumb_background,
Edges::default(),
Hsla::transparent_black(),
BorderStyle::default(),
));
}
if thumb_state.is_dragging() {
window.set_window_cursor_style(CursorStyle::Arrow);
@@ -361,13 +432,18 @@ impl Element for Scrollbar {
});
window.on_mouse_event({
let state = self.state.clone();
let scroll_handle = self.state.scroll_handle().clone();
move |event: &ScrollWheelEvent, phase, window, _| {
if phase.bubble() && bounds.contains(&event.position) {
let current_offset = scroll_handle.offset();
scroll_handle.set_offset(
current_offset + event.delta.pixel_delta(window.line_height()),
);
move |event: &ScrollWheelEvent, phase, window, cx| {
if phase.bubble() {
state.unhide(&event.position, cx);
if bounds.contains(&event.position) {
let current_offset = scroll_handle.offset();
scroll_handle.set_offset(
current_offset + event.delta.pixel_delta(window.line_height()),
);
}
}
}
});
@@ -376,6 +452,8 @@ impl Element for Scrollbar {
let state = self.state.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if phase.bubble() {
state.unhide(&event.position, cx);
match state.thumb_state.get() {
ThumbState::Dragging(drag_state) if event.dragging() => {
let scroll_handle = state.scroll_handle();

View File

@@ -71,8 +71,4 @@ impl Model {
Model::Custom { .. } => false,
}
}
pub fn supports_prompt_cache_key(&self) -> bool {
false
}
}

View File

@@ -105,10 +105,6 @@ impl Model {
}
}
pub fn supports_prompt_cache_key(&self) -> bool {
false
}
pub fn supports_tool(&self) -> bool {
match self {
Self::Grok2Vision

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.199.9"
version = "0.200.0"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
stable
dev

View File

@@ -5,9 +5,11 @@ use editor::Editor;
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity};
use language::language_settings::{EditPredictionProvider, all_language_settings};
use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{cell::RefCell, rc::Rc, sync::Arc};
use supermaven::{Supermaven, SupermavenCompletionProvider};
use ui::Window;
use util::ResultExt;
use workspace::Workspace;
use zeta::{ProviderDataCollection, ZetaEditPredictionProvider};
@@ -57,20 +59,25 @@ pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
cx.on_action(clear_zeta_edit_history);
let mut provider = all_language_settings(None, cx).edit_predictions.provider;
cx.subscribe(&user_store, {
cx.spawn({
let user_store = user_store.clone();
let editors = editors.clone();
let client = client.clone();
move |user_store, event, cx| match event {
client::user::Event::PrivateUserInfoUpdated => {
assign_edit_prediction_providers(
&editors,
provider,
&client,
user_store.clone(),
cx,
);
async move |cx| {
let mut status = client.status();
while let Some(_status) = status.next().await {
cx.update(|cx| {
assign_edit_prediction_providers(
&editors,
provider,
&client,
user_store.clone(),
cx,
);
})
.log_err();
}
_ => {}
}
})
.detach();

View File

@@ -26,7 +26,6 @@ collections.workspace = true
command_palette_hooks.workspace = true
copilot.workspace = true
db.workspace = true
edit_prediction.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
@@ -34,13 +33,13 @@ futures.workspace = true
gpui.workspace = true
http_client.workspace = true
indoc.workspace = true
edit_prediction.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
menu.workspace = true
postage.workspace = true
project.workspace = true
rand.workspace = true
regex.workspace = true
release_channel.workspace = true
serde.workspace = true

View File

@@ -432,7 +432,6 @@ impl Zeta {
body,
editable_range,
} = gather_task.await?;
let done_gathering_context_at = Instant::now();
log::debug!(
"Events:\n{}\nExcerpt:\n{:?}",
@@ -485,7 +484,6 @@ impl Zeta {
}
};
let received_response_at = Instant::now();
log::debug!("completion response: {}", &response.output_excerpt);
if let Some(usage) = usage {
@@ -497,7 +495,7 @@ impl Zeta {
.ok();
}
let edit_prediction = Self::process_completion_response(
Self::process_completion_response(
response,
buffer,
&snapshot,
@@ -510,25 +508,7 @@ impl Zeta {
buffer_snapshotted_at,
&cx,
)
.await;
let finished_at = Instant::now();
// record latency for ~1% of requests
if rand::random::<u8>() <= 2 {
telemetry::event!(
"Edit Prediction Request",
context_latency = done_gathering_context_at
.duration_since(buffer_snapshotted_at)
.as_millis(),
request_latency = received_response_at
.duration_since(done_gathering_context_at)
.as_millis(),
process_latency = finished_at.duration_since(received_response_at).as_millis()
);
}
edit_prediction
.await
})
}

View File

@@ -14,25 +14,25 @@ You can add your API key to a given provider either via the Agent Panel's settin
Here's all the supported LLM providers for which you can use your own API keys:
| Provider | Tool Use Supported |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Amazon Bedrock](#amazon-bedrock) | Depends on the model |
| [Anthropic](#anthropic) | ✅ |
| [DeepSeek](#deepseek) | ✅ |
| [GitHub Copilot Chat](#github-copilot-chat) | For some models ([link](https://github.com/zed-industries/zed/blob/9e0330ba7d848755c9734bf456c716bddf0973f3/crates/language_models/src/provider/copilot_chat.rs#L189-L198)) |
| [Google AI](#google-ai) | ✅ |
| [LM Studio](#lmstudio) | ✅ |
| [Mistral](#mistral) | ✅ |
| [Ollama](#ollama) | ✅ |
| [OpenAI](#openai) | ✅ |
| [OpenAI API Compatible](#openai-api-compatible) | ✅ |
| [OpenRouter](#openrouter) | ✅ |
| [Vercel](#vercel-v0) | ✅ |
| [xAI](#xai) | ✅ |
| Provider |
| ----------------------------------------------- |
| [Amazon Bedrock](#amazon-bedrock) |
| [Anthropic](#anthropic) |
| [DeepSeek](#deepseek) |
| [GitHub Copilot Chat](#github-copilot-chat) |
| [Google AI](#google-ai) |
| [LM Studio](#lmstudio) |
| [Mistral](#mistral) |
| [Ollama](#ollama) |
| [OpenAI](#openai) |
| [OpenAI API Compatible](#openai-api-compatible) |
| [OpenRouter](#openrouter) |
| [Vercel](#vercel-v0) |
| [xAI](#xai) |
### Amazon Bedrock {#amazon-bedrock}
> Supports tool use with models that support streaming tool use.
> Supports tool use with models that support streaming tool use.
> More details can be found in the [Amazon Bedrock's Tool Use documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html).
To use Amazon Bedrock's models, an AWS authentication is required.
@@ -107,8 +107,6 @@ For the most up-to-date supported regions and models, refer to the [Supported Mo
### Anthropic {#anthropic}
> ✅ Supports tool use
You can use Anthropic models by choosing them via the model dropdown in the Agent Panel.
1. Sign up for Anthropic and [create an API key](https://console.anthropic.com/settings/keys)
@@ -165,8 +163,6 @@ You can configure a model to use [extended thinking](https://docs.anthropic.com/
### DeepSeek {#deepseek}
> ✅ Supports tool use
1. Visit the DeepSeek platform and [create an API key](https://platform.deepseek.com/api_keys)
2. Open the settings view (`agent: open settings`) and go to the DeepSeek section
3. Enter your DeepSeek API key
@@ -208,9 +204,6 @@ You can also modify the `api_url` to use a custom endpoint if needed.
### GitHub Copilot Chat {#github-copilot-chat}
> ✅ Supports tool use in some cases.
> Visit [the Copilot Chat code](https://github.com/zed-industries/zed/blob/9e0330ba7d848755c9734bf456c716bddf0973f3/crates/language_models/src/provider/copilot_chat.rs#L189-L198) for the supported subset.
You can use GitHub Copilot Chat with the Zed agent by choosing it via the model dropdown in the Agent Panel.
1. Open the settings view (`agent: open settings`) and go to the GitHub Copilot Chat section
@@ -224,8 +217,6 @@ To use Copilot Enterprise with Zed (for both agent and completions), you must co
### Google AI {#google-ai}
> ✅ Supports tool use
You can use Gemini models with the Zed agent by choosing it via the model dropdown in the Agent Panel.
1. Go to the Google AI Studio site and [create an API key](https://aistudio.google.com/app/apikey).
@@ -266,8 +257,6 @@ Custom models will be listed in the model dropdown in the Agent Panel.
### LM Studio {#lmstudio}
> ✅ Supports tool use
1. Download and install [the latest version of LM Studio](https://lmstudio.ai/download)
2. In the app press `cmd/ctrl-shift-m` and download at least one model (e.g., qwen2.5-coder-7b). Alternatively, you can get models via the LM Studio CLI:
@@ -285,8 +274,6 @@ Tip: Set [LM Studio as a login item](https://lmstudio.ai/docs/advanced/headless#
### Mistral {#mistral}
> ✅ Supports tool use
1. Visit the Mistral platform and [create an API key](https://console.mistral.ai/api-keys/)
2. Open the configuration view (`agent: open settings`) and navigate to the Mistral section
3. Enter your Mistral API key
@@ -326,8 +313,6 @@ Custom models will be listed in the model dropdown in the Agent Panel.
### Ollama {#ollama}
> ✅ Supports tool use
Download and install Ollama from [ollama.com/download](https://ollama.com/download) (Linux or macOS) and ensure it's running with `ollama --version`.
1. Download one of the [available models](https://ollama.com/models), for example, for `mistral`:
@@ -395,8 +380,6 @@ If the model is tagged with `vision` in the Ollama catalog, set this option and
### OpenAI {#openai}
> ✅ Supports tool use
1. Visit the OpenAI platform and [create an API key](https://platform.openai.com/account/api-keys)
2. Make sure that your OpenAI account has credits
3. Open the settings view (`agent: open settings`) and go to the OpenAI section
@@ -473,8 +456,6 @@ So, ensure you have it set in your environment variables (`OPENAI_API_KEY=<your
### OpenRouter {#openrouter}
> ✅ Supports tool use
OpenRouter provides access to multiple AI models through a single API. It supports tool use for compatible models.
1. Visit [OpenRouter](https://openrouter.ai) and create an account
@@ -531,8 +512,6 @@ Custom models will be listed in the model dropdown in the Agent Panel.
### Vercel v0 {#vercel-v0}
> ✅ Supports tool use
[Vercel v0](https://vercel.com/docs/v0/api) is an expert model for generating full-stack apps, with framework-aware completions optimized for modern stacks like Next.js and Vercel.
It supports text and image inputs and provides fast streaming responses.
@@ -545,8 +524,6 @@ You should then find it as `v0-1.5-md` in the model dropdown in the Agent Panel.
### xAI {#xai}
> ✅ Supports tool use
Zed has first-class support for [xAI](https://x.ai/) models. You can use your own API key to access Grok models.
1. [Create an API key in the xAI Console](https://console.x.ai/team/default/api-keys)

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