Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5de2c28f75 | ||
|
|
2ecb5b2ff6 | ||
|
|
f1af9d5fbd | ||
|
|
d97e15dcaf | ||
|
|
5db22c9440 | ||
|
|
49ef4b5024 | ||
|
|
cace7de723 | ||
|
|
3925aa9b29 | ||
|
|
7f9adae3a3 | ||
|
|
4b94e90899 | ||
|
|
63cc3291e3 | ||
|
|
f1e69f6311 | ||
|
|
bd1c26cb5b | ||
|
|
1907b16fe6 | ||
|
|
c595a7576d | ||
|
|
8e290b446e | ||
|
|
58392b9c13 | ||
|
|
9358690337 | ||
|
|
3ea90e397b | ||
|
|
a5dd8d0052 | ||
|
|
250c51bb20 | ||
|
|
010441e23b | ||
|
|
f9038f6189 | ||
|
|
a80da784b7 | ||
|
|
fb1f9d1212 | ||
|
|
794098e5c9 | ||
|
|
b08e26df60 | ||
|
|
740597492b | ||
|
|
ebda6b8a94 | ||
|
|
55b4df4d9f | ||
|
|
b8e8fbd8e6 | ||
|
|
33f198fef1 | ||
|
|
3c602fecbf | ||
|
|
334bdd0efc | ||
|
|
69dc870828 | ||
|
|
22fa41e9c0 | ||
|
|
7e790f52c8 |
35
.github/actionlint.yml
vendored
35
.github/actionlint.yml
vendored
@@ -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
|
||||
|
||||
2
.github/actions/build_docs/action.yml
vendored
2
.github/actions/build_docs/action.yml
vendored
@@ -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
132
.github/workflows/agent_servers_e2e.yml
vendored
Normal 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, I’ve included the cleanup code here as a precaution.
|
||||
# While it’s not strictly necessary at this moment, I believe it’s better to err on the side of caution.
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
2
.github/workflows/bump_patch_version.yml
vendored
2
.github/workflows/bump_patch_version.yml
vendored
@@ -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
|
||||
|
||||
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@@ -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: |
|
||||
( 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:
|
||||
|
||||
2
.github/workflows/deploy_cloudflare.yml
vendored
2
.github/workflows/deploy_cloudflare.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/deploy_collab.yml
vendored
4
.github/workflows/deploy_collab.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/eval.yml
vendored
4
.github/workflows/eval.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/nix.yml
vendored
2
.github/workflows/nix.yml
vendored
@@ -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]
|
||||
|
||||
2
.github/workflows/randomized_tests.yml
vendored
2
.github/workflows/randomized_tests.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/release_nightly.yml
vendored
4
.github/workflows/release_nightly.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/unit_evals.yml
vendored
4
.github/workflows/unit_evals.yml
vendored
@@ -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
10
Cargo.lock
generated
@@ -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.5"
|
||||
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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
1
Procfile
1
Procfile
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 776 B After Width: | Height: | Size: 776 B |
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)) => {
|
||||
|
||||
@@ -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!");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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, ¶ms)
|
||||
.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>,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ impl TestServer {
|
||||
client_name,
|
||||
Principal::User(user),
|
||||
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||
Some("test".to_string()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(_) => {
|
||||
|
||||
@@ -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>(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,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 {
|
||||
@@ -473,7 +455,6 @@ pub fn into_open_ai(
|
||||
} else {
|
||||
None
|
||||
},
|
||||
prompt_cache_key: request.thread_id,
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
@@ -693,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)
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name = "YAML"
|
||||
grammar = "yaml"
|
||||
path_suffixes = ["yml", "yaml"]
|
||||
path_suffixes = ["yml", "yaml", "pixi.lock"]
|
||||
line_comments = ["# "]
|
||||
autoclose_before = ",]}"
|
||||
brackets = [
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
@@ -111,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}'"),
|
||||
}
|
||||
}
|
||||
@@ -132,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,
|
||||
}
|
||||
}
|
||||
@@ -153,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),
|
||||
@@ -176,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,
|
||||
}
|
||||
}
|
||||
@@ -200,9 +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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,10 +197,7 @@ 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,
|
||||
}
|
||||
}
|
||||
@@ -244,8 +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>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -447,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))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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 {
|
||||
@@ -2340,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, ¤t_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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2356,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.))
|
||||
@@ -2427,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);
|
||||
},
|
||||
))),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ impl RenderOnce for Disclosure {
|
||||
|
||||
impl Component for Disclosure {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Navigation
|
||||
ComponentScope::Input
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.199.5"
|
||||
version = "0.200.0"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
stable
|
||||
dev
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
# Local Collaboration
|
||||
|
||||
First, make sure you've installed Zed's dependencies for your platform:
|
||||
1. Ensure you have access to our cloud infrastructure. If you don't have access, you can't collaborate locally at this time.
|
||||
|
||||
- [macOS](./macos.md#backend-dependencies)
|
||||
- [Linux](./linux.md#backend-dependencies)
|
||||
- [Windows](./windows.md#backend-dependencies)
|
||||
2. Make sure you've installed Zed's dependencies for your platform:
|
||||
|
||||
- [macOS](#macos)
|
||||
- [Linux](#linux)
|
||||
- [Windows](#backend-windows)
|
||||
|
||||
Note that `collab` can be compiled only with MSVC toolchain on Windows
|
||||
|
||||
3. Clone down our cloud repository and follow the instructions in the cloud README
|
||||
|
||||
4. Setup the local database for your platform:
|
||||
|
||||
- [macOS & Linux](#database-unix)
|
||||
- [Windows](#database-windows)
|
||||
|
||||
5. Run collab:
|
||||
|
||||
- [macOS & Linux](#run-collab-unix)
|
||||
- [Windows](#run-collab-windows)
|
||||
|
||||
## Backend Dependencies
|
||||
|
||||
If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server:
|
||||
@@ -18,7 +32,7 @@ If you are developing collaborative features of Zed, you'll need to install the
|
||||
|
||||
You can install these dependencies natively or run them under Docker.
|
||||
|
||||
### MacOS
|
||||
### macOS
|
||||
|
||||
1. Install [Postgres.app](https://postgresapp.com) or [postgresql via homebrew](https://formulae.brew.sh/formula/postgresql@15):
|
||||
|
||||
@@ -76,7 +90,7 @@ docker compose up -d
|
||||
|
||||
Before you can run the `collab` server locally, you'll need to set up a `zed` Postgres database.
|
||||
|
||||
### On macOS and Linux
|
||||
### On macOS and Linux {#database-unix}
|
||||
|
||||
```sh
|
||||
script/bootstrap
|
||||
@@ -99,7 +113,7 @@ To use a different set of admin users, you can create your own version of that j
|
||||
}
|
||||
```
|
||||
|
||||
### On Windows
|
||||
### On Windows {#database-windows}
|
||||
|
||||
```powershell
|
||||
.\script\bootstrap.ps1
|
||||
@@ -107,7 +121,7 @@ To use a different set of admin users, you can create your own version of that j
|
||||
|
||||
## Testing collaborative features locally
|
||||
|
||||
### On macOS and Linux
|
||||
### On macOS and Linux {#run-collab-unix}
|
||||
|
||||
Ensure that Postgres is configured and running, then run Zed's collaboration server and the `livekit` dev server:
|
||||
|
||||
@@ -117,12 +131,16 @@ foreman start
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Alternatively, if you're not testing voice and screenshare, you can just run `collab`, and not the `livekit` dev server:
|
||||
Alternatively, if you're not testing voice and screenshare, you can just run `collab` and `cloud`, and not the `livekit` dev server:
|
||||
|
||||
```sh
|
||||
cargo run -p collab -- serve all
|
||||
```
|
||||
|
||||
```sh
|
||||
cd ../cloud; cargo make dev
|
||||
```
|
||||
|
||||
In a new terminal, run two or more instances of Zed.
|
||||
|
||||
```sh
|
||||
@@ -131,7 +149,7 @@ script/zed-local -3
|
||||
|
||||
This script starts one to four instances of Zed, depending on the `-2`, `-3` or `-4` flags. Each instance will be connected to the local `collab` server, signed in as a different user from `.admins.json` or `.admins.default.json`.
|
||||
|
||||
### On Windows
|
||||
### On Windows {#run-collab-windows}
|
||||
|
||||
Since `foreman` is not available on Windows, you can run the following commands in separate terminals:
|
||||
|
||||
@@ -151,6 +169,12 @@ Otherwise,
|
||||
.\path\to\livekit-serve.exe --dev
|
||||
```
|
||||
|
||||
You'll also need to start the cloud server:
|
||||
|
||||
```powershell
|
||||
cd ..\cloud; cargo make dev
|
||||
```
|
||||
|
||||
In a new terminal, run two or more instances of Zed.
|
||||
|
||||
```powershell
|
||||
@@ -161,7 +185,10 @@ Note that this requires `node.exe` to be in your `PATH`.
|
||||
|
||||
## Running a local collab server
|
||||
|
||||
If you want to run your own version of the zed collaboration service, you can, but note that this is still under development, and there is no good support for authentication nor extensions.
|
||||
> [!NOTE]
|
||||
> Because of recent changes to our authentication system, Zed will not be able to authenticate itself with, and therefore use, a local collab server.
|
||||
|
||||
If you want to run your own version of the zed collaboration service, you can, but note that this is still under development, and there is no support for authentication nor extensions.
|
||||
|
||||
Configuration is done through environment variables. By default it will read the configuration from [`.env.toml`](https://github.com/zed-industries/zed/blob/main/crates/collab/.env.toml) and you should use that as a guide for setting this up.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user