Compare commits
209 Commits
v0.184.9
...
keymap_edi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1ac358f9f | ||
|
|
8553c8c404 | ||
|
|
acf8e86164 | ||
|
|
5959efba91 | ||
|
|
527f1f4586 | ||
|
|
632f08d2a3 | ||
|
|
5589e78a69 | ||
|
|
128b7d2245 | ||
|
|
fbb0fe40ec | ||
|
|
7587340fb1 | ||
|
|
5073cba08d | ||
|
|
0a02869513 | ||
|
|
854c554580 | ||
|
|
59708ef56c | ||
|
|
152ea04a21 | ||
|
|
074b6965e2 | ||
|
|
edda386827 | ||
|
|
9767033985 | ||
|
|
edf78e770d | ||
|
|
8d77efa781 | ||
|
|
747a029487 | ||
|
|
c8685dc90f | ||
|
|
d566864891 | ||
|
|
75a9ed164b | ||
|
|
e364e48266 | ||
|
|
b4732235e3 | ||
|
|
b1395c5fdf | ||
|
|
1a4d7249f6 | ||
|
|
d7004030b3 | ||
|
|
2508e491d5 | ||
|
|
a09e5d255b | ||
|
|
33abf1ee7c | ||
|
|
f7a3e00074 | ||
|
|
24e47de02a | ||
|
|
15a83b5a10 | ||
|
|
fde1cc78a1 | ||
|
|
f2813f60ed | ||
|
|
4758173c33 | ||
|
|
d732a7d361 | ||
|
|
cf6b051b99 | ||
|
|
a376038803 | ||
|
|
2973bf188b | ||
|
|
386970c29a | ||
|
|
f7f44bfbed | ||
|
|
fd17f2d8ae | ||
|
|
5507958327 | ||
|
|
fa40353fc5 | ||
|
|
83b8530e1f | ||
|
|
5afb89ca93 | ||
|
|
6386336eee | ||
|
|
c168fc335c | ||
|
|
b2df395918 | ||
|
|
2b431d3e9d | ||
|
|
4812c9094b | ||
|
|
fcef101227 | ||
|
|
7e25460708 | ||
|
|
9b37206147 | ||
|
|
756fcd0733 | ||
|
|
3fd37799b4 | ||
|
|
ab180855de | ||
|
|
2beefc8158 | ||
|
|
5092f0f18b | ||
|
|
3a212e72a4 | ||
|
|
4dc8ce8cf7 | ||
|
|
2cc5a0de26 | ||
|
|
bc665b2a76 | ||
|
|
17903a0999 | ||
|
|
5102c4c002 | ||
|
|
2139219832 | ||
|
|
9abeedf0c6 | ||
|
|
1d7c86bf0d | ||
|
|
17703310ae | ||
|
|
bbe8d6a654 | ||
|
|
bbc66748dd | ||
|
|
99df1190a9 | ||
|
|
0e477e7db9 | ||
|
|
0afb980f7b | ||
|
|
d360f77796 | ||
|
|
92b9bc599d | ||
|
|
ed367e1636 | ||
|
|
b41ffae161 | ||
|
|
ef33666701 | ||
|
|
cd86905ebe | ||
|
|
abb48b7711 | ||
|
|
8afac388bb | ||
|
|
53b36b328e | ||
|
|
ce93961fe0 | ||
|
|
e3c987e2fb | ||
|
|
4dc0551105 | ||
|
|
bf9e5b4f76 | ||
|
|
cfb8cae29c | ||
|
|
68e0105627 | ||
|
|
e98e6c7426 | ||
|
|
3a1bd38503 | ||
|
|
8a69d252f5 | ||
|
|
bf30beacc2 | ||
|
|
2a0be48875 | ||
|
|
1c4ba07b20 | ||
|
|
8a717abe0d | ||
|
|
f735c90c3f | ||
|
|
ddfeb202a3 | ||
|
|
9bd0828303 | ||
|
|
4dff47ae20 | ||
|
|
52eef3c35d | ||
|
|
f060918b57 | ||
|
|
609c528ceb | ||
|
|
6db974dd32 | ||
|
|
60ec55b179 | ||
|
|
bb7a5b13df | ||
|
|
1e47dfce79 | ||
|
|
3fdbc3090d | ||
|
|
f2b4004c00 | ||
|
|
ec5821f76d | ||
|
|
e22cae6459 | ||
|
|
21bafd7856 | ||
|
|
ee74edbbb1 | ||
|
|
d832b8e687 | ||
|
|
539f4f1576 | ||
|
|
9a325a23e5 | ||
|
|
ce31312268 | ||
|
|
d46890978a | ||
|
|
67615b968b | ||
|
|
053fafa90e | ||
|
|
d23024609f | ||
|
|
3961d87ae0 | ||
|
|
8b910e1cd9 | ||
|
|
12c645e154 | ||
|
|
cfb7a30724 | ||
|
|
7623fce4b4 | ||
|
|
7f5c874a38 | ||
|
|
8cc2ade21c | ||
|
|
c3177e6f5b | ||
|
|
c3570fbcf3 | ||
|
|
3aa313010f | ||
|
|
5f9c91d05a | ||
|
|
6692bd9f2b | ||
|
|
cc57bc7c96 | ||
|
|
c157b1c455 | ||
|
|
136e83e0b1 | ||
|
|
b28756ae3f | ||
|
|
65401d6d7b | ||
|
|
a5405fcbd7 | ||
|
|
4f9cadabf7 | ||
|
|
7443f89a2e | ||
|
|
9bee765d7f | ||
|
|
8c553ee9f0 | ||
|
|
3389327df5 | ||
|
|
f106dfca42 | ||
|
|
37fa437990 | ||
|
|
9be7bf72a4 | ||
|
|
357e38b471 | ||
|
|
ae37f3ca2e | ||
|
|
49003d8038 | ||
|
|
93862838bd | ||
|
|
c39adc5242 | ||
|
|
ebb39d9231 | ||
|
|
187f851613 | ||
|
|
a77db45865 | ||
|
|
6bb6be826d | ||
|
|
7d9a55d101 | ||
|
|
57d8397f53 | ||
|
|
17ecf94f6f | ||
|
|
d492939bed | ||
|
|
720dfee803 | ||
|
|
a98c648201 | ||
|
|
c147daae4a | ||
|
|
d3911e34de | ||
|
|
87f85f1863 | ||
|
|
1a4dab97db | ||
|
|
cd365b0cf5 | ||
|
|
58604fba86 | ||
|
|
b0609272c0 | ||
|
|
a17807d8b1 | ||
|
|
f81e65ae7c | ||
|
|
952fe34aaa | ||
|
|
f527df6fa1 | ||
|
|
b54bbebc03 | ||
|
|
8bb7a1f9e7 | ||
|
|
e70d8d4dfd | ||
|
|
ea5ce2a1a4 | ||
|
|
fd8eeb537d | ||
|
|
92f21ee39d | ||
|
|
fcfeea4825 | ||
|
|
c0f8e0f605 | ||
|
|
9d10489607 | ||
|
|
8836c6fb42 | ||
|
|
f125353b6f | ||
|
|
fef2681cfa | ||
|
|
8b5835de17 | ||
|
|
2124b7ea99 | ||
|
|
74442b68ea | ||
|
|
ba3d82629e | ||
|
|
ecc600a68f | ||
|
|
218496744c | ||
|
|
d095bab8ad | ||
|
|
f8c3fe7871 | ||
|
|
aa161078fb | ||
|
|
f11c749353 | ||
|
|
40b5a1b028 | ||
|
|
2d43818c04 | ||
|
|
636c6e7f2d | ||
|
|
45d3f5168a | ||
|
|
8366cd0b52 | ||
|
|
f6774ae60d | ||
|
|
92e810bfec | ||
|
|
724c935196 | ||
|
|
ef54b58346 | ||
|
|
01bdd170ec | ||
|
|
4b9f4feff1 |
11
.github/workflows/eval.yml
vendored
11
.github/workflows/eval.yml
vendored
@@ -2,7 +2,7 @@ name: Run Agent Eval
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -25,6 +25,15 @@ env:
|
||||
ZED_EVAL_TELEMETRY: 1
|
||||
|
||||
jobs:
|
||||
# This is a no-op job that we run to prevent GitHub from marking the workflow
|
||||
# as failed for PRs that don't have the `run-eval` label.
|
||||
noop:
|
||||
name: No-op
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: No-op
|
||||
run: echo "Nothing to do"
|
||||
|
||||
run_eval:
|
||||
timeout-minutes: 60
|
||||
name: Run Agent Eval
|
||||
|
||||
49
.github/workflows/release_nightly.yml
vendored
49
.github/workflows/release_nightly.yml
vendored
@@ -170,55 +170,6 @@ jobs:
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly linux-targz
|
||||
|
||||
bundle-nix:
|
||||
timeout-minutes: 60
|
||||
name: (${{ matrix.system.os }}) Nix Build
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
system:
|
||||
- os: x86 Linux
|
||||
runner: buildjet-16vcpu-ubuntu-2204
|
||||
install_nix: true
|
||||
- os: arm Mac
|
||||
runner: [macOS, ARM64, test]
|
||||
install_nix: false
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ${{ matrix.system.runner }}
|
||||
needs: tests
|
||||
env:
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
GIT_LFS_SKIP_SMUDGE: 1 # breaks the livekit rust sdk examples which we don't actually depend on
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
# on our macs we manually install nix. for some reason the cachix action is running
|
||||
# under a non-login /bin/bash shell which doesn't source the proper script to add the
|
||||
# nix profile to PATH, so we manually add them here
|
||||
- name: Set path
|
||||
if: ${{ ! matrix.system.install_nix }}
|
||||
run: |
|
||||
echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH
|
||||
echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH
|
||||
|
||||
- uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31
|
||||
if: ${{ matrix.system.install_nix }}
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
name: zed-industries
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
- run: nix build
|
||||
- name: Limit /nix/store to 50GB
|
||||
run: '[ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d'
|
||||
|
||||
update-nightly-tag:
|
||||
name: Update nightly tag
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,6 +23,7 @@
|
||||
/crates/theme/schemas/theme.json
|
||||
/crates/zed/resources/flatpak/flatpak-cargo-sources.json
|
||||
/dev.zed.Zed*.json
|
||||
/node_modules/
|
||||
/plugins/bin
|
||||
/script/node_modules
|
||||
/snap
|
||||
@@ -32,4 +33,5 @@ Packages
|
||||
xcuserdata/
|
||||
|
||||
# Don't commit any secrets to the repo.
|
||||
.env
|
||||
.env.secret.toml
|
||||
|
||||
5
.rules
5
.rules
@@ -119,3 +119,8 @@ GPUI has had some changes to its APIs. Always write code using the new APIs:
|
||||
* Use `App` references. This replaces `AppContext` which no longer exists and should NEVER be used.
|
||||
* Use `Context<T>` references. This replaces `ModelContext<T>` which no longer exists and should NEVER be used.
|
||||
* `Window` is now passed around explicitly. The new interface adds a `Window` reference parameter to some methods, and adds some new "*_in" methods for plumbing `Window`. The old types `WindowContext` and `ViewContext<T>` should NEVER be used.
|
||||
|
||||
|
||||
## General guidelines
|
||||
|
||||
- Use `./script/clippy` instead of `cargo clippy`
|
||||
|
||||
138
Cargo.lock
generated
138
Cargo.lock
generated
@@ -94,6 +94,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"picker",
|
||||
"postage",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
@@ -3161,6 +3162,7 @@ dependencies = [
|
||||
"go_to_line",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"picker",
|
||||
"postage",
|
||||
@@ -3211,6 +3213,7 @@ dependencies = [
|
||||
"db",
|
||||
"gpui",
|
||||
"languages",
|
||||
"log",
|
||||
"notifications",
|
||||
"project",
|
||||
"serde",
|
||||
@@ -4040,6 +4043,7 @@ dependencies = [
|
||||
"http_client",
|
||||
"language",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
@@ -4074,6 +4078,7 @@ dependencies = [
|
||||
"dap",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp-types",
|
||||
"paths",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4547,6 +4552,12 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
@@ -4968,7 +4979,8 @@ dependencies = [
|
||||
"client",
|
||||
"collections",
|
||||
"context_server",
|
||||
"dirs 5.0.1",
|
||||
"dirs 4.0.0",
|
||||
"dotenv",
|
||||
"env_logger 0.11.8",
|
||||
"extension",
|
||||
"fs",
|
||||
@@ -4981,7 +4993,9 @@ dependencies = [
|
||||
"language_model",
|
||||
"language_models",
|
||||
"languages",
|
||||
"markdown",
|
||||
"node_runtime",
|
||||
"pathdiff",
|
||||
"paths",
|
||||
"project",
|
||||
"prompt_store",
|
||||
@@ -7101,6 +7115,7 @@ dependencies = [
|
||||
"editor",
|
||||
"file_icons",
|
||||
"gpui",
|
||||
"log",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -8235,7 +8250,7 @@ dependencies = [
|
||||
"prost 0.9.0",
|
||||
"prost-build 0.9.0",
|
||||
"prost-types 0.9.0",
|
||||
"reqwest 0.12.8",
|
||||
"reqwest 0.12.15",
|
||||
"serde",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -8364,7 +8379,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.95.1"
|
||||
source = "git+https://github.com/zed-industries/lsp-types?rev=1fff0dd12e2071c5667327394cfec163d2a466ab#1fff0dd12e2071c5667327394cfec163d2a466ab"
|
||||
source = "git+https://github.com/zed-industries/lsp-types?rev=c9c189f1c5dd53c624a419ce35bc77ad6a908d18#c9c189f1c5dd53c624a419ce35bc77ad6a908d18"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"serde",
|
||||
@@ -10093,7 +10108,7 @@ name = "perplexity"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"zed_extension_api 0.4.0",
|
||||
"zed_extension_api 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12009,8 +12024,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.8"
|
||||
source = "git+https://github.com/zed-industries/reqwest.git?rev=fd110f6998da16bbca97b6dddda9be7827c50e29#fd110f6998da16bbca97b6dddda9be7827c50e29"
|
||||
version = "0.12.15"
|
||||
source = "git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415#951c770a32f1998d6e999cef3e59e0013e6c4415"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes 1.10.1",
|
||||
@@ -12045,13 +12060,14 @@ dependencies = [
|
||||
"tokio-rustls 0.26.2",
|
||||
"tokio-socks",
|
||||
"tokio-util",
|
||||
"tower 0.5.2",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"windows-registry 0.2.0",
|
||||
"windows-registry 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12066,7 +12082,7 @@ dependencies = [
|
||||
"http_client_tls",
|
||||
"log",
|
||||
"regex",
|
||||
"reqwest 0.12.8",
|
||||
"reqwest 0.12.15",
|
||||
"serde",
|
||||
"smol",
|
||||
"tokio",
|
||||
@@ -13189,7 +13205,12 @@ dependencies = [
|
||||
"command_palette_hooks",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"log",
|
||||
"paths",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -14298,6 +14319,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
@@ -14307,6 +14329,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -15118,6 +15141,11 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
@@ -17204,13 +17232,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.2.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
||||
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||
dependencies = [
|
||||
"windows-result 0.2.0",
|
||||
"windows-strings 0.1.0",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-result 0.3.2",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -17261,6 +17289,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.0"
|
||||
@@ -17345,13 +17382,29 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.53.0",
|
||||
"windows_aarch64_msvc 0.53.0",
|
||||
"windows_i686_gnu 0.53.0",
|
||||
"windows_i686_gnullvm 0.53.0",
|
||||
"windows_i686_msvc 0.53.0",
|
||||
"windows_x86_64_gnu 0.53.0",
|
||||
"windows_x86_64_gnullvm 0.53.0",
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -17370,6 +17423,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -17388,6 +17447,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -17406,12 +17471,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -17430,6 +17507,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -17448,6 +17531,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -17466,6 +17555,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -17484,6 +17579,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.6"
|
||||
@@ -17994,6 +18095,7 @@ dependencies = [
|
||||
"subtle",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.100",
|
||||
"sync_wrapper 1.0.2",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"time-macros",
|
||||
@@ -18004,6 +18106,7 @@ dependencies = [
|
||||
"tokio-util",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
"tower 0.5.2",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tungstenite 0.26.2",
|
||||
@@ -18362,7 +18465,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.184.9"
|
||||
version = "0.185.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -18389,7 +18492,6 @@ dependencies = [
|
||||
"collab_ui",
|
||||
"collections",
|
||||
"command_palette",
|
||||
"command_palette_hooks",
|
||||
"component_preview",
|
||||
"copilot",
|
||||
"dap",
|
||||
@@ -18530,7 +18632,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -18590,7 +18692,7 @@ dependencies = [
|
||||
name = "zed_test_extension"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.4.0",
|
||||
"zed_extension_api 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
46
Cargo.toml
46
Cargo.toml
@@ -39,9 +39,9 @@ members = [
|
||||
"crates/credentials_provider",
|
||||
"crates/dap",
|
||||
"crates/dap_adapters",
|
||||
"crates/db",
|
||||
"crates/debugger_tools",
|
||||
"crates/debugger_ui",
|
||||
"crates/db",
|
||||
"crates/deepseek",
|
||||
"crates/diagnostics",
|
||||
"crates/docs_preprocessor",
|
||||
@@ -109,7 +109,6 @@ members = [
|
||||
"crates/project",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/rules_library",
|
||||
"crates/prompt_store",
|
||||
"crates/proto",
|
||||
"crates/recent_projects",
|
||||
@@ -123,6 +122,7 @@ members = [
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rules_library",
|
||||
"crates/schema_generator",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
@@ -229,6 +229,7 @@ auto_update_ui = { path = "crates/auto_update_ui" }
|
||||
aws_http_client = { path = "crates/aws_http_client" }
|
||||
bedrock = { path = "crates/bedrock" }
|
||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
buffer_diff = { path = "crates/buffer_diff" }
|
||||
call = { path = "crates/call" }
|
||||
channel = { path = "crates/channel" }
|
||||
cli = { path = "crates/cli" }
|
||||
@@ -248,11 +249,10 @@ credentials_provider = { path = "crates/credentials_provider" }
|
||||
dap = { path = "crates/dap" }
|
||||
dap_adapters = { path = "crates/dap_adapters" }
|
||||
db = { path = "crates/db" }
|
||||
debugger_ui = { path = "crates/debugger_ui" }
|
||||
debugger_tools = { path = "crates/debugger_tools" }
|
||||
debugger_ui = { path = "crates/debugger_ui" }
|
||||
deepseek = { path = "crates/deepseek" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
buffer_diff = { path = "crates/buffer_diff" }
|
||||
editor = { path = "crates/editor" }
|
||||
extension = { path = "crates/extension" }
|
||||
extension_host = { path = "crates/extension_host" }
|
||||
@@ -309,8 +309,8 @@ ollama = { path = "crates/ollama" }
|
||||
open_ai = { path = "crates/open_ai" }
|
||||
outline = { path = "crates/outline" }
|
||||
outline_panel = { path = "crates/outline_panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
panel = { path = "crates/panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
plugin_macros = { path = "crates/plugin_macros" }
|
||||
@@ -318,7 +318,6 @@ prettier = { path = "crates/prettier" }
|
||||
project = { path = "crates/project" }
|
||||
project_panel = { path = "crates/project_panel" }
|
||||
project_symbols = { path = "crates/project_symbols" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
prompt_store = { path = "crates/prompt_store" }
|
||||
proto = { path = "crates/proto" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
@@ -331,6 +330,7 @@ reqwest_client = { path = "crates/reqwest_client" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
@@ -417,7 +417,6 @@ bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
naga = { version = "23.1.0", features = ["wgsl-in"] }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
@@ -427,15 +426,16 @@ circular-buffer = "1.0"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
cocoa = "0.26"
|
||||
cocoa-foundation = "0.2.0"
|
||||
core-video = { version = "0.4.3", features = ["metal"] }
|
||||
convert_case = "0.8.0"
|
||||
core-foundation = "0.10.0"
|
||||
core-foundation-sys = "0.8.6"
|
||||
core-video = { version = "0.4.3", features = ["metal"] }
|
||||
ctor = "0.4.0"
|
||||
dashmap = "6.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
dotenv = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
@@ -452,8 +452,8 @@ heck = "0.5"
|
||||
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
html5ever = "0.27.0"
|
||||
hyper = "0.14"
|
||||
http = "1.1"
|
||||
hyper = "0.14"
|
||||
ignore = "0.4.22"
|
||||
image = "0.25.1"
|
||||
imara-diff = "0.1.8"
|
||||
@@ -469,24 +469,27 @@ libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
linkme = "0.3.31"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
metal = "0.29"
|
||||
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
|
||||
naga = { version = "23.1.0", features = ["wgsl-in"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
objc = "0.2"
|
||||
open = "5.0.0"
|
||||
num-format = "0.4.4"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
partial-json-fixer = "0.5.3"
|
||||
pathdiff = "0.2"
|
||||
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
@@ -499,11 +502,11 @@ prost-types = "0.9"
|
||||
pulldown-cmark = { version = "0.12.0", default-features = false }
|
||||
quote = "1.0.9"
|
||||
rand = "0.8.5"
|
||||
ref-cast = "1.0.24"
|
||||
rayon = "1.8"
|
||||
ref-cast = "1.0.24"
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = [
|
||||
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c770a32f1998d6e999cef3e59e0013e6c4415", default-features = false, features = [
|
||||
"charset",
|
||||
"http2",
|
||||
"macos-system-configuration",
|
||||
@@ -515,8 +518,8 @@ rsa = "0.9.6"
|
||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rustc-demangle = "0.1.23"
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
rustc-demangle = "0.1.23"
|
||||
rustc-hash = "2.1.0"
|
||||
rustls = { version = "0.23.26" }
|
||||
rustls-platform-verifier = "0.5.0"
|
||||
@@ -558,15 +561,16 @@ time = { version = "0.3", features = [
|
||||
"formatting",
|
||||
] }
|
||||
tiny_http = "0.8"
|
||||
toml = "0.8"
|
||||
tokio = { version = "1" }
|
||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
||||
toml = "0.8"
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.3", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.23"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
tree-sitter-css = "0.23"
|
||||
tree-sitter-diff = "0.1.0"
|
||||
tree-sitter-elixir = "0.3"
|
||||
tree-sitter-embedded-template = "0.23.0"
|
||||
tree-sitter-gitcommit = { git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9" }
|
||||
@@ -574,7 +578,6 @@ tree-sitter-go = "0.23"
|
||||
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
|
||||
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
|
||||
tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex", rev = "1dd45142fbb05562e35b2040c6129c9bca346592" }
|
||||
tree-sitter-diff = "0.1.0"
|
||||
tree-sitter-html = "0.23"
|
||||
tree-sitter-jsdoc = "0.23"
|
||||
tree-sitter-json = "0.24"
|
||||
@@ -586,15 +589,15 @@ tree-sitter-rust = "0.24"
|
||||
tree-sitter-typescript = "0.23"
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
|
||||
unicase = "2.6"
|
||||
unindent = "0.2.0"
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-script = "0.5.7"
|
||||
unicode-segmentation = "1.10"
|
||||
unindent = "0.2.0"
|
||||
url = "2.2"
|
||||
urlencoding = "2.1.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
walkdir = "2.3"
|
||||
wasmparser = "0.221"
|
||||
wasm-encoder = "0.221"
|
||||
wasmparser = "0.221"
|
||||
wasmtime = { version = "29", default-features = false, features = [
|
||||
"async",
|
||||
"demangle",
|
||||
@@ -608,7 +611,6 @@ wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.7.1"
|
||||
zstd = "0.11"
|
||||
metal = "0.29"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
git = "https://github.com/zed-industries/async-stripe"
|
||||
|
||||
5
assets/icons/menu_alt.svg
Normal file
5
assets/icons/menu_alt.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 12H16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 6H20" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 18H12" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 402 B |
@@ -60,8 +60,6 @@
|
||||
"shift-tab": "editor::Backtab",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
// "ctrl-t": "editor::Transpose",
|
||||
"ctrl-k ctrl-q": "editor::Rewrap",
|
||||
"ctrl-k q": "editor::Rewrap",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"cut": "editor::Cut",
|
||||
@@ -106,28 +104,7 @@
|
||||
"ctrl-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
"ctrl-l": "editor::SelectLine",
|
||||
"ctrl-shift-i": "editor::Format",
|
||||
"alt-shift-o": "editor::OrganizeImports",
|
||||
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-alt-space": "editor::ShowCharacterPalette",
|
||||
"ctrl-;": "editor::ToggleLineNumbers",
|
||||
"ctrl-'": "editor::ToggleSelectedDiffHunks",
|
||||
"ctrl-\"": "editor::ExpandAllDiffHunks",
|
||||
"ctrl-i": "editor::ShowSignatureHelp",
|
||||
"alt-g b": "editor::ToggleGitBlame",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
|
||||
"ctrl-alt-space": "editor::ShowCharacterPalette"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -146,7 +123,30 @@
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer",
|
||||
"ctrl-k ctrl-q": "editor::Rewrap",
|
||||
"ctrl-k q": "editor::Rewrap",
|
||||
"ctrl-shift-i": "editor::Format",
|
||||
"alt-shift-o": "editor::OrganizeImports",
|
||||
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-;": "editor::ToggleLineNumbers",
|
||||
"ctrl-'": "editor::ToggleSelectedDiffHunks",
|
||||
"ctrl-\"": "editor::ExpandAllDiffHunks",
|
||||
"ctrl-i": "editor::ShowSignatureHelp",
|
||||
"alt-g b": "editor::ToggleGitBlame",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-shift-backspace": "editor::GoToPreviousChange",
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -245,11 +245,19 @@
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"ctrl-shift-o": "agent::ToggleNavigationMenu",
|
||||
"ctrl-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-e": "agent::ChatMode",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > NavigationMenu",
|
||||
"bindings": {
|
||||
"shift-backspace": "agent::DeleteRecentlyOpenThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
@@ -437,8 +445,6 @@
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
|
||||
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
|
||||
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
@@ -446,10 +452,20 @@
|
||||
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
|
||||
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
|
||||
"ctrl-k ctrl-i": "editor::Hover",
|
||||
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"ctrl-\\": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle",
|
||||
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"ctrl-k ctrl-i": "editor::Hover",
|
||||
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
"shift-f8": "editor::GoToPreviousDiagnostic",
|
||||
"f2": "editor::Rename",
|
||||
@@ -483,7 +499,6 @@
|
||||
"ctrl-.": "editor::ToggleCodeActions",
|
||||
"ctrl-k r": "editor::RevealInFileManager",
|
||||
"ctrl-k p": "editor::CopyPath",
|
||||
"ctrl-\\": "pane::SplitRight",
|
||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
||||
@@ -491,13 +506,6 @@
|
||||
"alt-,": "editor::GoToPreviousHunk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
@@ -520,6 +528,7 @@
|
||||
"shift-new": "workspace::NewWindow",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
|
||||
"alt-1": ["workspace::ActivatePane", 0],
|
||||
"alt-2": ["workspace::ActivatePane", 1],
|
||||
"alt-3": ["workspace::ActivatePane", 2],
|
||||
@@ -578,6 +587,7 @@
|
||||
{
|
||||
"context": "ApplicationMenu",
|
||||
"bindings": {
|
||||
"f10": "menu::Cancel",
|
||||
"left": "app_menu::ActivateMenuLeft",
|
||||
"right": "app_menu::ActivateMenuRight"
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
// `Editor` context applies to all editor modes
|
||||
// - auto_height (multi-line: inline assistant, git commit messages, etc)
|
||||
// - single_line (command palette, renaming a file, etc)
|
||||
// - full (main editor buffers, assistant text threads)
|
||||
"context": "Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
@@ -76,8 +80,6 @@
|
||||
"ctrl-t": "editor::Transpose",
|
||||
"ctrl-k": "editor::KillRingCut",
|
||||
"ctrl-y": "editor::KillRingYank",
|
||||
"cmd-k cmd-q": "editor::Rewrap",
|
||||
"cmd-k q": "editor::Rewrap",
|
||||
"cmd-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"cmd-delete": "editor::DeleteToEndOfLine",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
@@ -106,7 +108,6 @@
|
||||
"left": "editor::MoveLeft",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"right": "editor::MoveRight",
|
||||
"ctrl-l": "editor::ScrollCursorCenter",
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
"cmd-left": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
@@ -135,8 +136,6 @@
|
||||
"cmd-shift-down": "editor::SelectToEnd",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-l": "editor::SelectLine",
|
||||
"cmd-shift-i": "editor::Format",
|
||||
"alt-shift-o": "editor::OrganizeImports",
|
||||
"cmd-shift-left": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
"ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
|
||||
@@ -145,17 +144,7 @@
|
||||
"ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
|
||||
"ctrl-v": ["editor::MovePageDown", { "center_cursor": true }],
|
||||
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-cmd-space": "editor::ShowCharacterPalette",
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
||||
"cmd-\"": "editor::ExpandAllDiffHunks",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||
"cmd-i": "editor::ShowSignatureHelp",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-f12": "editor::GoToDeclaration",
|
||||
"alt-ctrl-f12": "editor::GoToDeclarationSplit",
|
||||
"ctrl-cmd-e": "editor::ToggleEditPrediction"
|
||||
"ctrl-cmd-space": "editor::ShowCharacterPalette"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -174,7 +163,22 @@
|
||||
"cmd->": "assistant::QuoteSelection",
|
||||
"cmd-<": "assistant::InsertIntoEditor",
|
||||
"cmd-alt-e": "editor::SelectEnclosingSymbol",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer"
|
||||
"cmd-k cmd-q": "editor::Rewrap",
|
||||
"cmd-k q": "editor::Rewrap",
|
||||
"ctrl-l": "editor::ScrollCursorCenter",
|
||||
"cmd-i": "editor::ShowSignatureHelp",
|
||||
"cmd-shift-i": "editor::Format",
|
||||
"alt-shift-o": "editor::OrganizeImports",
|
||||
"ctrl-cmd-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint",
|
||||
"ctrl-f12": "editor::GoToDeclaration",
|
||||
"alt-ctrl-f12": "editor::GoToDeclarationSplit",
|
||||
"alt-enter": "editor::OpenSelectionsInMultibuffer",
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
||||
"cmd-\"": "editor::ExpandAllDiffHunks",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -290,11 +294,19 @@
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
"cmd-shift-o": "agent::ToggleNavigationMenu",
|
||||
"cmd-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"cmd-e": "agent::ChatMode",
|
||||
"cmd-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > NavigationMenu",
|
||||
"bindings": {
|
||||
"shift-backspace": "agent::DeleteRecentlyOpenThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"use_key_equivalents": true,
|
||||
@@ -490,8 +502,6 @@
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"alt-shift-up": "editor::DuplicateLineUp",
|
||||
"alt-shift-down": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
@@ -501,21 +511,18 @@
|
||||
// defaults write com.apple.symbolichotkeys AppleSymbolicHotKeys -dict-add 70 '<dict><key>enabled</key><false/></dict>'
|
||||
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
|
||||
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
"cmd-shift-u": "editor::RedoSelection",
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
"shift-f8": "editor::GoToPreviousDiagnostic",
|
||||
"f2": "editor::Rename",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
"cmd-f12": "editor::GoToTypeDefinition",
|
||||
"shift-f12": "editor::GoToImplementation",
|
||||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"cmd-|": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"cmd-\\": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
// VSCode Bindings full Editors (e.g. buffers)
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-cmd-[": "editor::Fold",
|
||||
"alt-cmd-]": "editor::UnfoldLines",
|
||||
"cmd-k cmd-l": "editor::ToggleFold",
|
||||
@@ -539,22 +546,28 @@
|
||||
"cmd-.": "editor::ToggleCodeActions",
|
||||
"cmd-k r": "editor::RevealInFileManager",
|
||||
"cmd-k p": "editor::CopyPath",
|
||||
"cmd-\\": "pane::SplitRight",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
"shift-f8": "editor::GoToPreviousDiagnostic",
|
||||
"f2": "editor::Rename",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
"cmd-f12": "editor::GoToTypeDefinition",
|
||||
"shift-f12": "editor::GoToImplementation",
|
||||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames",
|
||||
"cmd-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle",
|
||||
"cmd-shift-backspace": "editor::GoToPreviousChange",
|
||||
"cmd-shift-alt-backspace": "editor::GoToNextChange"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -10,11 +10,6 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
|
||||
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
|
||||
"ctrl-b": "editor::GoToDefinition", // fuzzy-finder:toggle-buffer-finder
|
||||
"ctrl-alt-b": "editor::GoToDefinitionSplit", // N/A: From JetBrains
|
||||
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
|
||||
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
|
||||
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
|
||||
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
|
||||
@@ -25,14 +20,19 @@
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
|
||||
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
|
||||
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
|
||||
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
|
||||
"ctrl-\\": "workspace::ToggleLeftDock" // tree-view:toggle (overrides bind in default keymap)
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
|
||||
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
|
||||
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
|
||||
"ctrl-b": "editor::GoToDefinition", // fuzzy-finder:toggle-buffer-finder
|
||||
"ctrl-alt-b": "editor::GoToDefinitionSplit", // N/A: From JetBrains
|
||||
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
|
||||
"ctrl-r": "outline::Toggle", // symbols-view:toggle-project-symbols
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32,24 +32,15 @@
|
||||
"ctrl-alt-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::MoveLineUp",
|
||||
"ctrl-shift-down": "editor::MoveLineDown",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-d": "editor::DuplicateSelection",
|
||||
"alt-f3": "editor::SelectAllMatches", // find_all_under
|
||||
// "ctrl-f3": "", // find_under (cancels any selections)
|
||||
// "cmd-alt-shift-g": "" // find_under_prev (cancels any selections)
|
||||
"f9": "editor::SortLinesCaseSensitive",
|
||||
"ctrl-f9": "editor::SortLinesCaseInsensitive",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPreviousHunk",
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"f3": "editor::FindNextMatch",
|
||||
@@ -59,7 +50,16 @@
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-r": "outline::Toggle"
|
||||
"ctrl-r": "outline::Toggle",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPreviousHunk",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -10,11 +10,6 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-l": "language_selector::Toggle",
|
||||
"cmd-|": "pane::RevealInProjectPanel",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"alt-cmd-b": "editor::GoToDefinitionSplit",
|
||||
"cmd-<": "editor::ScrollCursorCenter",
|
||||
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
@@ -26,13 +21,18 @@
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-up": "editor::MoveLineUp",
|
||||
"ctrl-cmd-down": "editor::MoveLineDown",
|
||||
"cmd-\\": "workspace::ToggleLeftDock",
|
||||
"cmd-\\": "workspace::ToggleLeftDock", // overrides bind in default keymap
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-shift-l": "language_selector::Toggle",
|
||||
"cmd-|": "pane::RevealInProjectPanel",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"alt-cmd-b": "editor::GoToDefinitionSplit",
|
||||
"cmd-<": "editor::ScrollCursorCenter",
|
||||
"cmd-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -50,6 +50,12 @@
|
||||
"] -": "vim::NextLesserIndent",
|
||||
"] +": "vim::NextGreaterIndent",
|
||||
"] =": "vim::NextSameIndent",
|
||||
"] b": "pane::ActivateNextItem",
|
||||
"[ b": "pane::ActivatePreviousItem",
|
||||
"] shift-b": "pane::ActivateLastItem",
|
||||
"[ shift-b": ["pane::ActivateItem", 0],
|
||||
"] space": "vim::InsertEmptyLineBelow",
|
||||
"[ space": "vim::InsertEmptyLineAbove",
|
||||
// Word motions
|
||||
"w": "vim::NextWordStart",
|
||||
"e": "vim::NextWordEnd",
|
||||
@@ -108,7 +114,11 @@
|
||||
"ctrl-e": "vim::LineDown",
|
||||
"ctrl-y": "vim::LineUp",
|
||||
// "g" commands
|
||||
"g r": "vim::PushReplaceWithRegister",
|
||||
"g shift-r": "vim::PushReplaceWithRegister",
|
||||
"g r n": "editor::Rename",
|
||||
"g r r": "editor::FindAllReferences",
|
||||
"g r i": "editor::GoToImplementation",
|
||||
"g r a": "editor::ToggleCodeActions",
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g t": "pane::ActivateNextItem",
|
||||
@@ -127,6 +137,7 @@
|
||||
"g <": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"g a": "editor::SelectAllMatches",
|
||||
"g s": "outline::Toggle",
|
||||
"g shift-o": "outline::Toggle",
|
||||
"g shift-s": "project_symbols::Toggle",
|
||||
"g .": "editor::ToggleCodeActions", // zed specific
|
||||
"g shift-a": "editor::FindAllReferences", // zed specific
|
||||
@@ -305,7 +316,7 @@
|
||||
"!": "vim::ShellCommand",
|
||||
"i": ["vim::PushObject", { "around": false }],
|
||||
"a": ["vim::PushObject", { "around": true }],
|
||||
"g r": ["vim::Paste", { "preserve_clipboard": true }],
|
||||
"g shift-r": ["vim::Paste", { "preserve_clipboard": true }],
|
||||
"g c": "vim::ToggleComments",
|
||||
"g q": "vim::Rewrap",
|
||||
"g ?": "vim::ConvertToRot13",
|
||||
@@ -339,7 +350,8 @@
|
||||
"ctrl-shift-q": ["vim::PushLiteral", {}],
|
||||
"ctrl-r": "vim::PushRegister",
|
||||
"insert": "vim::ToggleReplace",
|
||||
"ctrl-o": "vim::TemporaryNormal"
|
||||
"ctrl-o": "vim::TemporaryNormal",
|
||||
"ctrl-s": "editor::ShowSignatureHelp"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -504,12 +516,14 @@
|
||||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
"q": "vim::AnyQuotes",
|
||||
// "q": "vim::AnyQuotes",
|
||||
"q": "vim::MiniQuotes",
|
||||
"|": "vim::VerticalBars",
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
"b": "vim::Parentheses",
|
||||
// "b": "vim::AnyBrackets",
|
||||
// "b": "vim::MiniBrackets",
|
||||
"[": "vim::SquareBrackets",
|
||||
"]": "vim::SquareBrackets",
|
||||
"r": "vim::SquareBrackets",
|
||||
@@ -630,9 +644,10 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gr",
|
||||
"context": "vim_operator == gR",
|
||||
"bindings": {
|
||||
"r": "vim::CurrentLine"
|
||||
"r": "vim::CurrentLine",
|
||||
"shift-r": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -693,7 +708,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||
"context": "GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel",
|
||||
"bindings": {
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w": null,
|
||||
|
||||
@@ -15,6 +15,7 @@ You are a highly skilled software engineer with extensive knowledge in many prog
|
||||
3. DO NOT use tools to access items that are already available in the context section.
|
||||
4. Use only the tools that are currently available.
|
||||
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
||||
6. NEVER run commands that don't terminate on their own such as web servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers.
|
||||
|
||||
## Searching and Reading
|
||||
|
||||
@@ -38,18 +39,78 @@ If appropriate, use tool calls to explore the current project, which contains th
|
||||
|
||||
## Code Block Formatting
|
||||
|
||||
Whenever you mention a code block, you MUST use ONLY use the following format when the code in the block comes from a file
|
||||
in the project:
|
||||
|
||||
Whenever you mention a code block, you MUST use ONLY use the following format:
|
||||
```path/to/Something.blah#L123-456
|
||||
(code goes here)
|
||||
```
|
||||
|
||||
The `#L123-456` means the line number range 123 through 456, and the path/to/Something.blah
|
||||
is a path in the project. (If this code block does not come from a file in the project, then you may instead use
|
||||
the normal markdown style of three backticks followed by language name. However, you MUST use this format if
|
||||
the code in the block comes from a file in the project.)
|
||||
|
||||
is a path in the project. (If there is no valid path in the project, then you can use
|
||||
/dev/null/path.extension for its path.) This is the ONLY valid way to format code blocks, because the Markdown parser
|
||||
does not understand the more common ```language syntax, or bare ``` blocks. It only
|
||||
understands this path-based syntax, and if the path is missing, then it will error and you will have to do it over again.
|
||||
Just to be really clear about this, if you ever find yourself writing three backticks followed by a language name, STOP!
|
||||
You have made a mistake. You can only ever put paths after triple backticks!
|
||||
<example>
|
||||
Based on all the information I've gathered, here's a summary of how this system works:
|
||||
1. The README file is loaded into the system.
|
||||
2. The system finds the first two headers, including everything in between. In this case, that would be:
|
||||
```path/to/README.md#L8-12
|
||||
# First Header
|
||||
This is the info under the first header.
|
||||
## Sub-header
|
||||
```
|
||||
3. Then the system finds the last header in the README:
|
||||
```path/to/README.md#L27-29
|
||||
## Last Header
|
||||
This is the last header in the README.
|
||||
```
|
||||
4. Finally, it passes this information on to the next process.
|
||||
</example>
|
||||
<example>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
```/dev/null/example.md#L1-3
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</example>
|
||||
Here are examples of ways you must never render code blocks:
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
```
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
This example is unacceptable because it does not include the path.
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
```markdown
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
This example is unacceptable because it has the language instead of the path.
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
</bad_example_do_not_do_this>
|
||||
This example is unacceptable because it uses indentation to mark the code block
|
||||
instead of backticks with a path.
|
||||
<bad_example_do_not_do_this>
|
||||
In Markdown, hash marks signify headings. For example:
|
||||
```markdown
|
||||
/dev/null/example.md#L1-3
|
||||
# Level 1 heading
|
||||
## Level 2 heading
|
||||
### Level 3 heading
|
||||
```
|
||||
</bad_example_do_not_do_this>
|
||||
This example is unacceptable because the path is in the wrong place. The path must be directly after the opening backticks.
|
||||
## Fixing Diagnostics
|
||||
|
||||
1. Make 1-2 attempts at fixing diagnostics, then defer to the user.
|
||||
|
||||
@@ -226,7 +226,7 @@
|
||||
// Hide the values of in variables from visual display in private files
|
||||
"redact_private_values": false,
|
||||
// The default number of lines to expand excerpts in the multibuffer by.
|
||||
"expand_excerpt_lines": 3,
|
||||
"expand_excerpt_lines": 5,
|
||||
// Globs to match against file paths to determine if a file is private.
|
||||
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
@@ -601,6 +601,13 @@
|
||||
//
|
||||
// Default: main
|
||||
"fallback_branch_name": "main",
|
||||
|
||||
// Whether to sort entries in the panel by path
|
||||
// or by status (the default).
|
||||
//
|
||||
// Default: false
|
||||
"sort_by_path": false,
|
||||
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the git panel.
|
||||
//
|
||||
@@ -695,6 +702,11 @@
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
},
|
||||
"manual": {
|
||||
"name": "Manual",
|
||||
"enable_all_context_servers": false,
|
||||
"tools": {}
|
||||
}
|
||||
},
|
||||
// Where to show notifications when an agent has either completed
|
||||
|
||||
@@ -60,6 +60,7 @@ ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
rules_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
|
||||
@@ -23,16 +23,17 @@ use gpui::{
|
||||
Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
|
||||
linear_color_stop, linear_gradient, list, percentage, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language::{Buffer, Language, LanguageRegistry};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent,
|
||||
RequestUsage, Role, StopReason,
|
||||
LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent, RequestUsage, Role,
|
||||
StopReason,
|
||||
};
|
||||
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
|
||||
use project::{ProjectEntryId, ProjectItem as _};
|
||||
use rope::Point;
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
@@ -346,130 +347,130 @@ fn render_markdown_code_block(
|
||||
.child(Label::new("untitled").size(LabelSize::Small))
|
||||
.into_any_element(),
|
||||
),
|
||||
CodeBlockKind::FencedLang(raw_language_name) => Some(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(
|
||||
parsed_markdown
|
||||
.languages_by_name
|
||||
.get(raw_language_name)
|
||||
.and_then(|language| {
|
||||
language
|
||||
.config()
|
||||
.matcher
|
||||
.path_suffixes
|
||||
.iter()
|
||||
.find_map(|extension| {
|
||||
file_icons::FileIcons::get_icon(Path::new(extension), cx)
|
||||
})
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
parsed_markdown
|
||||
.languages_by_name
|
||||
.get(raw_language_name)
|
||||
.map(|language| language.name().into())
|
||||
.clone()
|
||||
.unwrap_or_else(|| raw_language_name.clone()),
|
||||
)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
CodeBlockKind::FencedLang(raw_language_name) => Some(render_code_language(
|
||||
parsed_markdown.languages_by_name.get(raw_language_name),
|
||||
raw_language_name.clone(),
|
||||
cx,
|
||||
)),
|
||||
CodeBlockKind::FencedSrc(path_range) => path_range.path.file_name().map(|file_name| {
|
||||
let content = if let Some(parent) = path_range.path.parent() {
|
||||
h_flex()
|
||||
.ml_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(file_name.to_string_lossy().to_string()).size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new(parent.to_string_lossy().to_string())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(path_range.path.to_string_lossy().to_string())
|
||||
.size(LabelSize::Small)
|
||||
.ml_1()
|
||||
.into_any_element()
|
||||
};
|
||||
// We tell the model to use /dev/null for the path instead of using ```language
|
||||
// because otherwise it consistently fails to use code citations.
|
||||
if path_range.path.starts_with("/dev/null") {
|
||||
let ext = path_range
|
||||
.path
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(|str| SharedString::new(str.to_string()))
|
||||
.unwrap_or_default();
|
||||
|
||||
h_flex()
|
||||
.id(("code-block-header-label", ix))
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.px_1()
|
||||
.gap_0p5()
|
||||
.cursor_pointer()
|
||||
.rounded_sm()
|
||||
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.children(
|
||||
file_icons::FileIcons::get_icon(&path_range.path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(content)
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Ignored),
|
||||
),
|
||||
render_code_language(
|
||||
parsed_markdown
|
||||
.languages_by_path
|
||||
.get(&path_range.path)
|
||||
.or_else(|| parsed_markdown.languages_by_name.get(&ext)),
|
||||
ext,
|
||||
cx,
|
||||
)
|
||||
.on_click({
|
||||
let path_range = path_range.clone();
|
||||
move |_, window, cx| {
|
||||
workspace
|
||||
.update(cx, {
|
||||
|workspace, cx| {
|
||||
let Some(project_path) = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.find_project_path(&path_range.path, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(target) = path_range.range.as_ref().map(|range| {
|
||||
Point::new(
|
||||
// Line number is 1-based
|
||||
range.start.line.saturating_sub(1),
|
||||
range.start.col.unwrap_or(0),
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let open_task =
|
||||
workspace.open_path(project_path, None, true, window, cx);
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let item = open_task.await?;
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
active_editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
target, window, cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
let content = if let Some(parent) = path_range.path.parent() {
|
||||
h_flex()
|
||||
.ml_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new(file_name.to_string_lossy().to_string())
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new(parent.to_string_lossy().to_string())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(path_range.path.to_string_lossy().to_string())
|
||||
.size(LabelSize::Small)
|
||||
.ml_1()
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(("code-block-header-label", ix))
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.px_1()
|
||||
.gap_0p5()
|
||||
.cursor_pointer()
|
||||
.rounded_sm()
|
||||
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.children(
|
||||
file_icons::FileIcons::get_icon(&path_range.path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(content)
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Ignored),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let path_range = path_range.clone();
|
||||
move |_, window, cx| {
|
||||
workspace
|
||||
.update(cx, {
|
||||
|workspace, cx| {
|
||||
let Some(project_path) = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.find_project_path(&path_range.path, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(target) = path_range.range.as_ref().map(|range| {
|
||||
Point::new(
|
||||
// Line number is 1-based
|
||||
range.start.line.saturating_sub(1),
|
||||
range.start.col.unwrap_or(0),
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let open_task = workspace.open_path(
|
||||
project_path,
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let item = open_task.await?;
|
||||
if let Some(active_editor) =
|
||||
item.downcast::<Editor>()
|
||||
{
|
||||
active_editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
target, window, cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -604,6 +605,32 @@ fn render_markdown_code_block(
|
||||
)
|
||||
}
|
||||
|
||||
fn render_code_language(
|
||||
language: Option<&Arc<Language>>,
|
||||
name_fallback: SharedString,
|
||||
cx: &App,
|
||||
) -> AnyElement {
|
||||
let icon_path = language.and_then(|language| {
|
||||
language
|
||||
.config()
|
||||
.matcher
|
||||
.path_suffixes
|
||||
.iter()
|
||||
.find_map(|extension| file_icons::FileIcons::get_icon(Path::new(extension), cx))
|
||||
.map(Icon::from_path)
|
||||
});
|
||||
|
||||
let language_label = language
|
||||
.map(|language| language.name().into())
|
||||
.unwrap_or(name_fallback);
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(icon_path.map(|icon| icon.color(Color::Muted).size(IconSize::Small)))
|
||||
.child(Label::new(language_label).size(LabelSize::Small))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn open_markdown_link(
|
||||
text: SharedString,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -682,7 +709,7 @@ fn open_markdown_link(
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.open_thread(&thread_id, window, cx)
|
||||
.open_thread_by_id(&thread_id, window, cx)
|
||||
.detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
@@ -702,7 +729,7 @@ struct EditMessageState {
|
||||
editor: Entity<Editor>,
|
||||
last_estimated_token_count: Option<usize>,
|
||||
_subscription: Subscription,
|
||||
_update_token_count_task: Option<Task<anyhow::Result<()>>>,
|
||||
_update_token_count_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl ActiveThread {
|
||||
@@ -903,6 +930,11 @@ impl ActiveThread {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::CancelEditing => {
|
||||
if self.editing_message.is_some() {
|
||||
self.cancel_editing_message(&menu::Cancel, window, cx);
|
||||
}
|
||||
}
|
||||
ThreadEvent::ShowError(error) => {
|
||||
self.last_error = Some(error.clone());
|
||||
}
|
||||
@@ -1247,7 +1279,7 @@ impl ActiveThread {
|
||||
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
|
||||
state._update_token_count_task.take();
|
||||
|
||||
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
let Some(configured_model) = self.thread.read(cx).configured_model() else {
|
||||
state.last_estimated_token_count.take();
|
||||
return;
|
||||
};
|
||||
@@ -1263,58 +1295,65 @@ impl ActiveThread {
|
||||
.await;
|
||||
}
|
||||
|
||||
let token_count = if let Some(task) = cx.update(|cx| {
|
||||
let Some(message) = thread.read(cx).message(message_id) else {
|
||||
log::error!("Message that was being edited no longer exists");
|
||||
return None;
|
||||
};
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
let token_count = if let Some(task) = cx
|
||||
.update(|cx| {
|
||||
let Some(message) = thread.read(cx).message(message_id) else {
|
||||
log::error!("Message that was being edited no longer exists");
|
||||
return None;
|
||||
};
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
|
||||
if message_text.is_empty() && message.loaded_context.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if message_text.is_empty() && message.loaded_context.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
message
|
||||
.loaded_context
|
||||
.add_to_request_message(&mut request_message);
|
||||
message
|
||||
.loaded_context
|
||||
.add_to_request_message(&mut request_message);
|
||||
|
||||
if !message_text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(message_text));
|
||||
}
|
||||
if !message_text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(message_text));
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
Some(default_model.model.count_tokens(request, cx))
|
||||
})? {
|
||||
task.await?
|
||||
Some(configured_model.model.count_tokens(request, cx))
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
task.await.log_err()
|
||||
} else {
|
||||
0
|
||||
Some(0)
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let Some((_message_id, state)) = this.editing_message.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if let Some(token_count) = token_count {
|
||||
this.update(cx, |this, cx| {
|
||||
let Some((_message_id, state)) = this.editing_message.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
|
||||
})
|
||||
state.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
|
||||
})
|
||||
.ok();
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1333,7 +1372,7 @@ impl ActiveThread {
|
||||
return;
|
||||
};
|
||||
let edited_text = state.editor.read(cx).text(cx);
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
let thread_model = self.thread.update(cx, |thread, cx| {
|
||||
thread.edit_message(
|
||||
message_id,
|
||||
Role::User,
|
||||
@@ -1343,9 +1382,10 @@ impl ActiveThread {
|
||||
for message_id in self.messages_after(message_id) {
|
||||
thread.delete_message(*message_id, cx);
|
||||
}
|
||||
thread.get_or_init_configured_model(cx)
|
||||
});
|
||||
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
let Some(model) = thread_model else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1508,8 +1548,6 @@ impl ActiveThread {
|
||||
|
||||
let show_feedback = thread.is_turn_end(ix);
|
||||
|
||||
let needs_confirmation = tool_uses.iter().any(|tool_use| tool_use.needs_confirmation);
|
||||
|
||||
let generating_label = (is_generating && is_last_message)
|
||||
.then(|| AnimatedLabel::new("Generating").size(LabelSize::Small));
|
||||
|
||||
@@ -1532,10 +1570,11 @@ impl ActiveThread {
|
||||
});
|
||||
|
||||
// For all items that should be aligned with the LLM's response.
|
||||
const RESPONSE_PADDING_X: Pixels = px(18.);
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
|
||||
let feedback_container = h_flex()
|
||||
.group("feedback_container")
|
||||
.mt_1()
|
||||
.py_2()
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.gap_1()
|
||||
@@ -1659,7 +1698,7 @@ impl ActiveThread {
|
||||
if let Some(edit_message_editor) = edit_message_editor.clone() {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = TextSize::Small.rems(cx);
|
||||
let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
|
||||
let line_height = font_size.to_pixels(window.rem_size()) * 1.75;
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
@@ -1743,8 +1782,7 @@ impl ActiveThread {
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.pt_3()
|
||||
.p_2p5()
|
||||
.gap_1()
|
||||
.children(message_content)
|
||||
.when_some(edit_message_editor.clone(), |this, edit_editor| {
|
||||
@@ -1914,7 +1952,7 @@ impl ActiveThread {
|
||||
parent.child(self.render_rules_item(cx))
|
||||
})
|
||||
.child(styled_message)
|
||||
.when(!needs_confirmation && generating_label.is_some(), |this| {
|
||||
.when(generating_label.is_some(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.h_8()
|
||||
@@ -3237,7 +3275,7 @@ pub(crate) fn open_context(
|
||||
panel.update(cx, |panel, cx| {
|
||||
let thread_id = thread_context.thread.read(cx).id().clone();
|
||||
panel
|
||||
.open_thread(&thread_id, window, cx)
|
||||
.open_thread_by_id(&thread_id, window, cx)
|
||||
.detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -597,6 +597,10 @@ impl Item for AgentDiff {
|
||||
editor.added_to_workspace(workspace, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Agent Diff".into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentDiff {
|
||||
@@ -947,6 +951,7 @@ mod tests {
|
||||
ThemeSettings::register(cx);
|
||||
ContextServerSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
language_model::init_settings(cx);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
@@ -41,7 +41,7 @@ use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal}
|
||||
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::context::{ContextLoadResult, LoadedContext};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::thread::{Message, Thread, ThreadEvent};
|
||||
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
|
||||
pub use crate::thread_store::ThreadStore;
|
||||
pub use agent_diff::{AgentDiff, AgentDiffToolbar};
|
||||
|
||||
@@ -50,6 +50,9 @@ actions!(
|
||||
[
|
||||
NewTextThread,
|
||||
ToggleContextPicker,
|
||||
ToggleNavigationMenu,
|
||||
ToggleOptionsMenu,
|
||||
DeleteRecentlyOpenThread,
|
||||
ToggleProfileSelector,
|
||||
RemoveAllContext,
|
||||
ExpandMessageEditor,
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{
|
||||
AgentProfile, AgentProfileContent, AgentProfileId, AssistantSettings, AssistantSettingsContent,
|
||||
ContextServerPresetContent, VersionedAssistantSettingsContent,
|
||||
ContextServerPresetContent,
|
||||
};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use fs::Fs;
|
||||
@@ -201,10 +201,10 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
let profile_id = self.profile_id.clone();
|
||||
let default_profile = self.profile.clone();
|
||||
let tool = tool.clone();
|
||||
move |settings, _cx| match settings {
|
||||
AssistantSettingsContent::Versioned(boxed) => {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
move |settings: &mut AssistantSettingsContent, _cx| {
|
||||
settings
|
||||
.v2_setting(|v2_settings| {
|
||||
let profiles = v2_settings.profiles.get_or_insert_default();
|
||||
let profile =
|
||||
profiles
|
||||
.entry(profile_id)
|
||||
@@ -240,9 +240,10 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
*preset.tools.entry(tool.name.clone()).or_default() = is_enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use assistant_settings::AssistantSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
|
||||
use crate::Thread;
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
@@ -9,7 +11,11 @@ use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
|
||||
pub use language_model_selector::ModelType;
|
||||
#[derive(Clone)]
|
||||
pub enum ModelType {
|
||||
Default(Entity<Thread>),
|
||||
InlineAssistant,
|
||||
}
|
||||
|
||||
pub struct AssistantModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
@@ -24,18 +30,39 @@ impl AssistantModelSelector {
|
||||
focus_handle: FocusHandle,
|
||||
model_type: ModelType,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
selector: cx.new(|cx| {
|
||||
selector: cx.new(move |cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
{
|
||||
let model_type = model_type.clone();
|
||||
move |cx| match &model_type {
|
||||
ModelType::Default(thread) => thread.read(cx).configured_model(),
|
||||
ModelType::InlineAssistant => {
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
}
|
||||
}
|
||||
},
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
|
||||
match model_type {
|
||||
ModelType::Default => {
|
||||
match &model_type {
|
||||
ModelType::Default(thread) => {
|
||||
thread.update(cx, |thread, cx| {
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(provider) = registry.provider(&model.provider_id())
|
||||
{
|
||||
thread.set_configured_model(
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
@@ -58,7 +85,6 @@ impl AssistantModelSelector {
|
||||
}
|
||||
}
|
||||
},
|
||||
model_type,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -18,8 +18,8 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
|
||||
Corner, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels,
|
||||
Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
|
||||
Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
|
||||
Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
|
||||
@@ -41,15 +41,16 @@ use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
|
||||
|
||||
use crate::active_thread::{ActiveThread, ActiveThreadEvent};
|
||||
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
|
||||
use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
|
||||
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::UsageBanner;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
|
||||
OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
|
||||
AddContextServer, AgentDiff, DeleteRecentlyOpenThread, ExpandMessageEditor, InlineAssistant,
|
||||
NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent,
|
||||
ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
|
||||
};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -104,6 +105,22 @@ pub fn init(cx: &mut App) {
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
@@ -113,6 +130,7 @@ pub fn init(cx: &mut App) {
|
||||
enum ActiveView {
|
||||
Thread {
|
||||
change_title_editor: Entity<Editor>,
|
||||
thread: WeakEntity<Thread>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
},
|
||||
PromptEditor {
|
||||
@@ -130,7 +148,7 @@ impl ActiveView {
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_text(summary, window, cx);
|
||||
editor.set_text(summary.clone(), window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -176,6 +194,7 @@ impl ActiveView {
|
||||
|
||||
Self::Thread {
|
||||
change_title_editor: editor,
|
||||
thread: thread.downgrade(),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -268,6 +287,7 @@ pub struct AssistantPanel {
|
||||
thread: Entity<ActiveThread>,
|
||||
message_editor: Entity<MessageEditor>,
|
||||
_active_thread_subscriptions: Vec<Subscription>,
|
||||
_default_model_subscription: Subscription,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
configuration: Option<Entity<AssistantConfiguration>>,
|
||||
@@ -278,6 +298,8 @@ pub struct AssistantPanel {
|
||||
history_store: Entity<HistoryStore>,
|
||||
history: Entity<ThreadHistory>,
|
||||
assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_navigation_menu: Option<Entity<ContextMenu>>,
|
||||
width: Option<Pixels>,
|
||||
height: Option<Pixels>,
|
||||
}
|
||||
@@ -379,8 +401,14 @@ impl AssistantPanel {
|
||||
}
|
||||
});
|
||||
|
||||
let history_store =
|
||||
cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
|
||||
let history_store = cx.new(|cx| {
|
||||
HistoryStore::new(
|
||||
thread_store.clone(),
|
||||
context_store.clone(),
|
||||
[RecentEntry::Thread(thread.clone())],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
@@ -391,7 +419,7 @@ impl AssistantPanel {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
let thread = cx.new(|cx| {
|
||||
let active_thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
thread_store.clone(),
|
||||
@@ -402,12 +430,127 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription = cx.subscribe(&thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
let active_thread_subscription =
|
||||
cx.subscribe(&active_thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
let weak_panel = weak_self.clone();
|
||||
|
||||
window.defer(cx, move |window, cx| {
|
||||
let panel = weak_panel.clone();
|
||||
let assistant_navigation_menu =
|
||||
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
|
||||
let recently_opened = panel
|
||||
.update(cx, |this, cx| {
|
||||
this.history_store.update(cx, |history_store, cx| {
|
||||
history_store.recently_opened_entries(cx)
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if !recently_opened.is_empty() {
|
||||
menu = menu.header("Recently Opened");
|
||||
|
||||
for entry in recently_opened.iter() {
|
||||
let summary = entry.summary(cx);
|
||||
menu = menu.entry_with_end_slot(
|
||||
summary,
|
||||
None,
|
||||
{
|
||||
let panel = panel.clone();
|
||||
let entry = entry.clone();
|
||||
move |window, cx| {
|
||||
panel
|
||||
.update(cx, {
|
||||
let entry = entry.clone();
|
||||
move |this, cx| match entry {
|
||||
RecentEntry::Thread(thread) => {
|
||||
this.open_thread(thread, window, cx)
|
||||
}
|
||||
RecentEntry::Context(context) => {
|
||||
let Some(path) = context.read(cx).path()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
this.open_saved_prompt_editor(
|
||||
path.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
IconName::Close,
|
||||
"Close Entry".into(),
|
||||
{
|
||||
let panel = panel.clone();
|
||||
let entry = entry.clone();
|
||||
move |_window, cx| {
|
||||
panel
|
||||
.update(cx, |this, cx| {
|
||||
this.history_store.update(
|
||||
cx,
|
||||
|history_store, cx| {
|
||||
history_store.remove_recently_opened_entry(
|
||||
&entry, cx,
|
||||
);
|
||||
},
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
}
|
||||
|
||||
menu.action("View All", Box::new(OpenHistory))
|
||||
.end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
|
||||
.fixed_width(px(320.).into())
|
||||
.keep_open_on_confirm(false)
|
||||
.key_context("NavigationMenu")
|
||||
});
|
||||
weak_panel
|
||||
.update(cx, |panel, cx| {
|
||||
cx.subscribe_in(
|
||||
&assistant_navigation_menu,
|
||||
window,
|
||||
|_, menu, _: &DismissEvent, window, cx| {
|
||||
menu.update(cx, |menu, _| {
|
||||
menu.clear_selected();
|
||||
});
|
||||
cx.focus_self(window);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
panel.assistant_navigation_menu = Some(assistant_navigation_menu);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
let _default_model_subscription = cx.subscribe(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
|this, _, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::DefaultModelChanged => {
|
||||
this.thread
|
||||
.read(cx)
|
||||
.thread()
|
||||
.clone()
|
||||
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
active_view,
|
||||
workspace,
|
||||
@@ -416,13 +559,14 @@ impl AssistantPanel {
|
||||
fs: fs.clone(),
|
||||
language_registry,
|
||||
thread_store: thread_store.clone(),
|
||||
thread,
|
||||
thread: active_thread,
|
||||
message_editor,
|
||||
_active_thread_subscriptions: vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
],
|
||||
_default_model_subscription,
|
||||
context_store,
|
||||
prompt_store,
|
||||
configuration: None,
|
||||
@@ -435,6 +579,8 @@ impl AssistantPanel {
|
||||
history_store: history_store.clone(),
|
||||
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
|
||||
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_navigation_menu: None,
|
||||
width: None,
|
||||
height: None,
|
||||
}
|
||||
@@ -629,13 +775,13 @@ impl AssistantPanel {
|
||||
|
||||
pub(crate) fn open_saved_prompt_editor(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let context = self
|
||||
.context_store
|
||||
.update(cx, |store, cx| store.open_local_context(path.clone(), cx));
|
||||
.update(cx, |store, cx| store.open_local_context(path, cx));
|
||||
let fs = self.fs.clone();
|
||||
let project = self.project.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
@@ -669,7 +815,7 @@ impl AssistantPanel {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn open_thread(
|
||||
pub(crate) fn open_thread_by_id(
|
||||
&mut self,
|
||||
thread_id: &ThreadId,
|
||||
window: &mut Window,
|
||||
@@ -678,73 +824,83 @@ impl AssistantPanel {
|
||||
let open_thread_task = self
|
||||
.thread_store
|
||||
.update(cx, |this, cx| this.open_thread(thread_id, cx));
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let thread = open_thread_task.await?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let thread_view = ActiveView::thread(thread.clone(), window, cx);
|
||||
this.set_active_view(thread_view, window, cx);
|
||||
let message_editor_context_store = cx.new(|_cx| {
|
||||
crate::context_store::ContextStore::new(
|
||||
this.project.downgrade(),
|
||||
Some(this.thread_store.downgrade()),
|
||||
)
|
||||
});
|
||||
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
||||
if let ThreadEvent::MessageAdded(_) = &event {
|
||||
// needed to leave empty state
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this.thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
this.thread_store.clone(),
|
||||
this.language_registry.clone(),
|
||||
this.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription =
|
||||
cx.subscribe(&this.thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this.message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
this.fs.clone(),
|
||||
this.workspace.clone(),
|
||||
message_editor_context_store,
|
||||
this.prompt_store.clone(),
|
||||
this.thread_store.downgrade(),
|
||||
thread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
this.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&this.message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this._active_thread_subscriptions = vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
];
|
||||
})
|
||||
this.open_thread(thread, window, cx);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn open_thread(
|
||||
&mut self,
|
||||
thread: Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let thread_view = ActiveView::thread(thread.clone(), window, cx);
|
||||
self.set_active_view(thread_view, window, cx);
|
||||
let message_editor_context_store = cx.new(|_cx| {
|
||||
crate::context_store::ContextStore::new(
|
||||
self.project.downgrade(),
|
||||
Some(self.thread_store.downgrade()),
|
||||
)
|
||||
});
|
||||
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
||||
if let ThreadEvent::MessageAdded(_) = &event {
|
||||
// needed to leave empty state
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self.thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
self.thread_store.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription =
|
||||
cx.subscribe(&self.thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self.message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
self.fs.clone(),
|
||||
self.workspace.clone(),
|
||||
message_editor_context_store,
|
||||
self.prompt_store.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
thread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self._active_thread_subscriptions = vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
];
|
||||
}
|
||||
|
||||
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match self.active_view {
|
||||
ActiveView::Configuration | ActiveView::History => {
|
||||
@@ -757,6 +913,24 @@ impl AssistantPanel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_navigation_menu(
|
||||
&mut self,
|
||||
_: &ToggleNavigationMenu,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.assistant_navigation_menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
pub fn toggle_options_menu(
|
||||
&mut self,
|
||||
_: &ToggleOptionsMenu,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.assistant_dropdown_menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
pub fn open_agent_diff(
|
||||
&mut self,
|
||||
_: &OpenAgentDiff,
|
||||
@@ -905,7 +1079,7 @@ impl AssistantPanel {
|
||||
|
||||
pub(crate) fn delete_context(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.context_store
|
||||
@@ -921,6 +1095,32 @@ impl AssistantPanel {
|
||||
let current_is_history = matches!(self.active_view, ActiveView::History);
|
||||
let new_is_history = matches!(new_view, ActiveView::History);
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
if thread.read(cx).is_empty() {
|
||||
store.remove_recently_opened_entry(&RecentEntry::Thread(thread), cx);
|
||||
}
|
||||
}
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match &new_view {
|
||||
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
store.push_recently_opened_entry(RecentEntry::Thread(thread), cx);
|
||||
}
|
||||
}),
|
||||
ActiveView::PromptEditor { context_editor, .. } => {
|
||||
self.history_store.update(cx, |store, cx| {
|
||||
let context = context_editor.read(cx).context().clone();
|
||||
store.push_recently_opened_entry(RecentEntry::Context(context), cx)
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if current_is_history && !new_is_history {
|
||||
self.active_view = new_view;
|
||||
} else if !current_is_history && new_is_history {
|
||||
@@ -1050,16 +1250,13 @@ impl AssistantPanel {
|
||||
if is_empty {
|
||||
Label::new(Thread::DEFAULT_SUMMARY.clone())
|
||||
.truncate()
|
||||
.ml_2()
|
||||
.into_any_element()
|
||||
} else if summary.is_none() {
|
||||
Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
||||
.ml_2()
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
} else {
|
||||
div()
|
||||
.ml_2()
|
||||
.w_full()
|
||||
.child(change_title_editor.clone())
|
||||
.into_any_element()
|
||||
@@ -1076,18 +1273,15 @@ impl AssistantPanel {
|
||||
match summary {
|
||||
None => Label::new(AssistantContext::DEFAULT_SUMMARY.clone())
|
||||
.truncate()
|
||||
.ml_2()
|
||||
.into_any_element(),
|
||||
Some(summary) => {
|
||||
if summary.done {
|
||||
div()
|
||||
.ml_2()
|
||||
.w_full()
|
||||
.child(title_editor.clone())
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
||||
.ml_2()
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -1114,7 +1308,6 @@ impl AssistantPanel {
|
||||
let thread = active_thread.thread().read(cx);
|
||||
let thread_id = thread.id().clone();
|
||||
let is_empty = active_thread.is_empty();
|
||||
let is_history = matches!(self.active_view, ActiveView::History);
|
||||
|
||||
let show_token_count = match &self.active_view {
|
||||
ActiveView::Thread { .. } => !is_empty,
|
||||
@@ -1124,30 +1317,108 @@ impl AssistantPanel {
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let go_back_button = match &self.active_view {
|
||||
ActiveView::History | ActiveView::Configuration => Some(
|
||||
div().pl_1().child(
|
||||
IconButton::new("go-back", IconName::ArrowLeft)
|
||||
let go_back_button = div().child(
|
||||
IconButton::new("go-back", IconName::ArrowLeft)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.go_back(&workspace::GoBack, window, cx);
|
||||
}))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Go Back",
|
||||
&workspace::GoBack,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let recent_entries_menu = div().child(
|
||||
PopoverMenu::new("agent-nav-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("agent-nav-menu", IconName::MenuAlt)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.go_back(&workspace::GoBack, window, cx);
|
||||
}))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Go Back",
|
||||
&workspace::GoBack,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
.style(ui::ButtonStyle::Subtle),
|
||||
{
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Panel Menu",
|
||||
&ToggleNavigationMenu,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.anchor(Corner::TopLeft)
|
||||
.with_handle(self.assistant_navigation_menu_handle.clone())
|
||||
.menu({
|
||||
let menu = self.assistant_navigation_menu.clone();
|
||||
move |window, cx| {
|
||||
if let Some(menu) = menu.as_ref() {
|
||||
menu.update(cx, |_, cx| {
|
||||
cx.defer_in(window, |menu, window, cx| {
|
||||
menu.rebuild(window, cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
menu.clone()
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let agent_extra_menu = PopoverMenu::new("agent-options-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
||||
.icon_size(IconSize::Small),
|
||||
{
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Agent Menu",
|
||||
&ToggleOptionsMenu,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.assistant_dropdown_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
menu.when(!is_empty, |menu| {
|
||||
menu.action(
|
||||
"Start New From Summary",
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread_id.clone()),
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
})
|
||||
.action("New Text Thread", NewTextThread.boxed_clone())
|
||||
.action("Rules Library", Box::new(OpenRulesLibrary::default()))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.separator()
|
||||
.header("MCPs")
|
||||
.action(
|
||||
"View Server Extensions",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
)
|
||||
.action("Add Custom Server", Box::new(AddContextServer))
|
||||
}))
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.id("assistant-toolbar")
|
||||
@@ -1161,18 +1432,22 @@ impl AssistantPanel {
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.size_full()
|
||||
.pl_1()
|
||||
.gap_1()
|
||||
.children(go_back_button)
|
||||
.child(match &self.active_view {
|
||||
ActiveView::History | ActiveView::Configuration => go_back_button,
|
||||
_ => recent_entries_menu,
|
||||
})
|
||||
.child(self.render_title_view(window, cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.gap_2()
|
||||
.when(show_token_count, |parent|
|
||||
.when(show_token_count, |parent| {
|
||||
parent.children(self.render_token_count(&thread, cx))
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
@@ -1200,72 +1475,7 @@ impl AssistantPanel {
|
||||
);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-history", IconName::HistoryRerun)
|
||||
.icon_size(IconSize::Small)
|
||||
.toggle_state(is_history)
|
||||
.selected_icon_color(Color::Accent)
|
||||
.tooltip({
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"History",
|
||||
&OpenHistory,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(OpenHistory.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
PopoverMenu::new("assistant-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("new", IconName::Ellipsis)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle),
|
||||
Tooltip::text("Toggle Agent Menu"),
|
||||
)
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.assistant_dropdown_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
Some(ContextMenu::build(
|
||||
window,
|
||||
cx,
|
||||
|menu, _window, _cx| {
|
||||
menu
|
||||
.when(!is_empty, |menu| {
|
||||
menu.action(
|
||||
"Start New From Summary",
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread_id.clone()),
|
||||
}),
|
||||
).separator()
|
||||
})
|
||||
.action(
|
||||
"New Text Thread",
|
||||
NewTextThread.boxed_clone(),
|
||||
)
|
||||
.action("Rules Library", Box::new(OpenRulesLibrary::default()))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.separator()
|
||||
.header("MCPs")
|
||||
.action(
|
||||
"View Server Extensions",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
}),
|
||||
)
|
||||
.action("Add Custom Server", Box::new(AddContextServer))
|
||||
},
|
||||
))
|
||||
}),
|
||||
),
|
||||
.child(agent_extra_menu),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1274,12 +1484,13 @@ impl AssistantPanel {
|
||||
let is_generating = thread.is_generating();
|
||||
let message_editor = self.message_editor.read(cx);
|
||||
|
||||
let conversation_token_usage = thread.total_token_usage(cx);
|
||||
let conversation_token_usage = thread.total_token_usage()?;
|
||||
|
||||
let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
|
||||
self.thread.read(cx).editing_message_id()
|
||||
{
|
||||
let combined = thread
|
||||
.token_usage_up_to_message(editing_message_id, cx)
|
||||
.token_usage_up_to_message(editing_message_id)
|
||||
.add(unsent_tokens);
|
||||
|
||||
(combined, unsent_tokens > 0)
|
||||
@@ -1965,6 +2176,8 @@ impl Render for AssistantPanel {
|
||||
.on_action(cx.listener(Self::deploy_rules_library))
|
||||
.on_action(cx.listener(Self::open_agent_diff))
|
||||
.on_action(cx.listener(Self::go_back))
|
||||
.on_action(cx.listener(Self::toggle_navigation_menu))
|
||||
.on_action(cx.listener(Self::toggle_options_menu))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::Thread { .. } => parent
|
||||
@@ -2049,7 +2262,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
fn open_saved_context(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
path: std::path::PathBuf,
|
||||
path: Arc<Path>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<Result<()>> {
|
||||
|
||||
@@ -504,7 +504,7 @@ impl CodegenAlternative {
|
||||
}
|
||||
}
|
||||
|
||||
let http_client = cx.http_client().clone();
|
||||
let http_client = cx.http_client();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let language_name = {
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
|
||||
@@ -542,12 +542,21 @@ impl ThreadContextHandle {
|
||||
}
|
||||
|
||||
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
|
||||
let context = AgentContext::Thread(ThreadContext {
|
||||
title: self.title(cx),
|
||||
text: self.thread.read(cx).latest_detailed_summary_or_text(),
|
||||
handle: self,
|
||||
});
|
||||
Task::ready(Some((context, vec![])))
|
||||
cx.spawn(async move |cx| {
|
||||
let text = Thread::wait_for_detailed_summary_or_text(&self.thread, cx).await?;
|
||||
let title = self
|
||||
.thread
|
||||
.read_with(cx, |thread, _cx| {
|
||||
thread.summary().unwrap_or_else(|| "New thread".into())
|
||||
})
|
||||
.ok()?;
|
||||
let context = AgentContext::Thread(ThreadContext {
|
||||
title,
|
||||
text,
|
||||
handle: self,
|
||||
});
|
||||
Some((context, vec![]))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ impl ContextPicker {
|
||||
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
|
||||
})
|
||||
}))
|
||||
.keep_open_on_confirm()
|
||||
.keep_open_on_confirm(true)
|
||||
});
|
||||
|
||||
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
|
||||
|
||||
@@ -708,7 +708,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
|
||||
let thread_store = self.thread_store.clone();
|
||||
let editor = self.editor.clone();
|
||||
let http_client = workspace.read(cx).client().http_client().clone();
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
|
||||
let MentionCompletion { mode, argument, .. } = state;
|
||||
let query = argument.unwrap_or_else(|| "".to_string());
|
||||
@@ -1045,6 +1045,10 @@ mod tests {
|
||||
fn include_in_nav_history() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Test".into()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for AtMentionEditor {}
|
||||
|
||||
@@ -193,7 +193,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
return;
|
||||
};
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client().clone();
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let url = self.url.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let text = cx
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use collections::{HashSet, IndexSet};
|
||||
use futures::future::join_all;
|
||||
use futures::{self, FutureExt};
|
||||
use gpui::{App, Context, Entity, Image, SharedString, Task, WeakEntity};
|
||||
use language::Buffer;
|
||||
@@ -14,7 +13,6 @@ use project::{Project, ProjectItem, ProjectPath, Symbol};
|
||||
use prompt_store::UserPromptId;
|
||||
use ref_cast::RefCast as _;
|
||||
use text::{Anchor, OffsetRangeExt};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{
|
||||
@@ -28,7 +26,6 @@ use crate::thread::{Thread, ThreadId};
|
||||
pub struct ContextStore {
|
||||
project: WeakEntity<Project>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
thread_summary_tasks: Vec<Task<()>>,
|
||||
next_context_id: ContextId,
|
||||
context_set: IndexSet<AgentContextKey>,
|
||||
context_thread_ids: HashSet<ThreadId>,
|
||||
@@ -42,7 +39,6 @@ impl ContextStore {
|
||||
Self {
|
||||
project,
|
||||
thread_store,
|
||||
thread_summary_tasks: Vec::new(),
|
||||
next_context_id: ContextId::zero(),
|
||||
context_set: IndexSet::default(),
|
||||
context_thread_ids: HashSet::default(),
|
||||
@@ -206,41 +202,6 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_summarizing_thread_if_needed(
|
||||
&mut self,
|
||||
thread: &Entity<Thread>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(summary_task) =
|
||||
thread.update(cx, |thread, cx| thread.generate_detailed_summary(cx))
|
||||
{
|
||||
let thread = thread.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
|
||||
self.thread_summary_tasks.push(cx.spawn(async move |_, cx| {
|
||||
summary_task.await;
|
||||
|
||||
if let Some(thread_store) = thread_store {
|
||||
// Save thread so its summary can be reused later
|
||||
let save_task = thread_store
|
||||
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx));
|
||||
|
||||
if let Some(save_task) = save_task.ok() {
|
||||
save_task.await.log_err();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wait_for_summaries(&mut self, cx: &App) -> Task<()> {
|
||||
let tasks = std::mem::take(&mut self.thread_summary_tasks);
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
join_all(tasks).await;
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_rules(
|
||||
&mut self,
|
||||
prompt_id: UserPromptId,
|
||||
@@ -378,9 +339,15 @@ impl ContextStore {
|
||||
fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context<Self>) -> bool {
|
||||
match &context {
|
||||
AgentContextHandle::Thread(thread_context) => {
|
||||
self.context_thread_ids
|
||||
.insert(thread_context.thread.read(cx).id().clone());
|
||||
self.start_summarizing_thread_if_needed(&thread_context.thread, cx);
|
||||
if let Some(thread_store) = self.thread_store.clone() {
|
||||
thread_context.thread.update(cx, |thread, cx| {
|
||||
thread.start_generating_detailed_summary_if_needed(thread_store, cx);
|
||||
});
|
||||
self.context_thread_ids
|
||||
.insert(thread_context.thread.read(cx).id().clone());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
use assistant_context_editor::SavedContextMetadata;
|
||||
use std::{collections::VecDeque, path::Path};
|
||||
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
|
||||
use chrono::{DateTime, Utc};
|
||||
use gpui::{Entity, prelude::*};
|
||||
use futures::future::{TryFutureExt as _, join_all};
|
||||
use gpui::{Entity, Task, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::future::FutureExt;
|
||||
use std::time::Duration;
|
||||
use ui::{App, SharedString};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
|
||||
use crate::{
|
||||
Thread,
|
||||
thread::ThreadId,
|
||||
thread_store::{SerializedThreadMetadata, ThreadStore},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
|
||||
const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
|
||||
const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum HistoryEntry {
|
||||
Thread(SerializedThreadMetadata),
|
||||
Context(SavedContextMetadata),
|
||||
@@ -19,16 +36,40 @@ impl HistoryEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum RecentEntry {
|
||||
Thread(Entity<Thread>),
|
||||
Context(Entity<AssistantContext>),
|
||||
}
|
||||
|
||||
impl RecentEntry {
|
||||
pub(crate) fn summary(&self, cx: &App) -> SharedString {
|
||||
match self {
|
||||
RecentEntry::Thread(thread) => thread.read(cx).summary_or_default(),
|
||||
RecentEntry::Context(context) => context.read(cx).summary_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum SerializedRecentEntry {
|
||||
Thread(String),
|
||||
Context(String),
|
||||
}
|
||||
|
||||
pub struct HistoryStore {
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
recently_opened_entries: VecDeque<RecentEntry>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
_save_recently_opened_entries_task: Task<()>,
|
||||
}
|
||||
|
||||
impl HistoryStore {
|
||||
pub fn new(
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = vec![
|
||||
@@ -36,10 +77,61 @@ impl HistoryStore {
|
||||
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
||||
];
|
||||
|
||||
cx.spawn({
|
||||
let thread_store = thread_store.downgrade();
|
||||
let context_store = context_store.downgrade();
|
||||
async move |this, cx| {
|
||||
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
|
||||
let contents = cx
|
||||
.background_spawn(async move { std::fs::read_to_string(path) })
|
||||
.await
|
||||
.context("reading persisted agent panel navigation history")?;
|
||||
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
|
||||
.context("deserializing persisted agent panel navigation history")?
|
||||
.into_iter()
|
||||
.take(MAX_RECENTLY_OPENED_ENTRIES)
|
||||
.map(|serialized| match serialized {
|
||||
SerializedRecentEntry::Thread(id) => thread_store
|
||||
.update(cx, |thread_store, cx| {
|
||||
thread_store
|
||||
.open_thread(&ThreadId::from(id.as_str()), cx)
|
||||
.map_ok(RecentEntry::Thread)
|
||||
.boxed()
|
||||
})
|
||||
.unwrap_or_else(|_| async { Err(anyhow!("no thread store")) }.boxed()),
|
||||
SerializedRecentEntry::Context(id) => context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store
|
||||
.open_local_context(Path::new(&id).into(), cx)
|
||||
.map_ok(RecentEntry::Context)
|
||||
.boxed()
|
||||
})
|
||||
.unwrap_or_else(|_| async { Err(anyhow!("no context store")) }.boxed()),
|
||||
});
|
||||
let entries = join_all(entries)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|result| result.log_err())
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.recently_opened_entries.extend(entries);
|
||||
this.recently_opened_entries
|
||||
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
|
||||
})
|
||||
.ok();
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
thread_store,
|
||||
context_store,
|
||||
recently_opened_entries: initial_recent_entries.into_iter().collect(),
|
||||
_subscriptions: subscriptions,
|
||||
_save_recently_opened_entries_task: Task::ready(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,4 +161,57 @@ impl HistoryStore {
|
||||
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
||||
self.entries(cx).into_iter().take(limit).collect()
|
||||
}
|
||||
|
||||
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
|
||||
let serialized_entries = self
|
||||
.recently_opened_entries
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
|
||||
context.read(cx).path()?.to_str()?.to_owned(),
|
||||
)),
|
||||
RecentEntry::Thread(thread) => Some(SerializedRecentEntry::Thread(
|
||||
thread.read(cx).id().to_string(),
|
||||
)),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
|
||||
cx.background_executor()
|
||||
.timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
|
||||
.await;
|
||||
cx.background_spawn(async move {
|
||||
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
|
||||
let content = serde_json::to_string(&serialized_entries)?;
|
||||
std::fs::write(path, content)?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
|
||||
self.recently_opened_entries
|
||||
.retain(|old_entry| old_entry != &entry);
|
||||
self.recently_opened_entries.push_front(entry);
|
||||
self.recently_opened_entries
|
||||
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
|
||||
pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
|
||||
self.recently_opened_entries
|
||||
.retain(|old_entry| old_entry != entry);
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
|
||||
pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
|
||||
return VecDeque::new();
|
||||
}
|
||||
|
||||
self.recently_opened_entries.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1341,7 +1341,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<InlineAssist>(
|
||||
row_range,
|
||||
cx.theme().status().info_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1406,7 +1406,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
cx.theme().status().deleted_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
||||
use crate::buffer_codegen::BufferCodegen;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
@@ -20,7 +20,7 @@ use gpui::{
|
||||
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use language_model_selector::{ModelType, ToggleModelSelector};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use parking_lot::Mutex;
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::assistant_model_selector::ModelType;
|
||||
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
||||
use crate::context::{ContextLoadResult, load_context};
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
use crate::ui::AnimatedLabel;
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashSet;
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
@@ -21,9 +22,7 @@ use gpui::{
|
||||
Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage, MessageContent,
|
||||
};
|
||||
use language_model::{ConfiguredModel, LanguageModelRequestMessage, MessageContent};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
@@ -36,7 +35,6 @@ use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
use zed_llm_client::CompletionMode;
|
||||
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
@@ -60,13 +58,12 @@ pub struct MessageEditor {
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
last_loaded_context: Option<ContextLoadResult>,
|
||||
context_load_task: Option<Shared<Task<()>>>,
|
||||
load_context_task: Option<Shared<Task<()>>>,
|
||||
profile_selector: Entity<ProfileSelector>,
|
||||
edits_expanded: bool,
|
||||
editor_is_expanded: bool,
|
||||
waiting_for_summaries_to_send: bool,
|
||||
last_estimated_token_count: Option<usize>,
|
||||
update_token_count_task: Option<Task<anyhow::Result<()>>>,
|
||||
update_token_count_task: Option<Task<()>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -149,10 +146,22 @@ impl MessageEditor {
|
||||
_ => {}
|
||||
}),
|
||||
cx.observe(&context_store, |this, _, cx| {
|
||||
let _ = this.start_context_load(cx);
|
||||
// When context changes, reload it for token counting.
|
||||
let _ = this.reload_context(cx);
|
||||
}),
|
||||
];
|
||||
|
||||
let model_selector = cx.new(|cx| {
|
||||
AssistantModelSelector::new(
|
||||
fs.clone(),
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
ModelType::Default(thread.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
editor: editor.clone(),
|
||||
project: thread.read(cx).project().clone(),
|
||||
@@ -163,21 +172,11 @@ impl MessageEditor {
|
||||
prompt_store,
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
context_load_task: None,
|
||||
load_context_task: None,
|
||||
last_loaded_context: None,
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(
|
||||
fs.clone(),
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
model_selector,
|
||||
edits_expanded: false,
|
||||
editor_is_expanded: false,
|
||||
waiting_for_summaries_to_send: false,
|
||||
profile_selector: cx
|
||||
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
|
||||
last_estimated_token_count: None,
|
||||
@@ -245,6 +244,10 @@ impl MessageEditor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_editing(cx);
|
||||
});
|
||||
|
||||
if self.thread.read(cx).is_generating() {
|
||||
self.stop_current_and_send_new_message(window, cx);
|
||||
return;
|
||||
@@ -260,15 +263,11 @@ impl MessageEditor {
|
||||
self.editor.read(cx).text(cx).trim().is_empty()
|
||||
}
|
||||
|
||||
fn is_model_selected(&self, cx: &App) -> bool {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {
|
||||
let Some(ConfiguredModel { model, provider }) = self
|
||||
.thread
|
||||
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -289,7 +288,7 @@ impl MessageEditor {
|
||||
let thread = self.thread.clone();
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
|
||||
let context_task = self.load_context(cx);
|
||||
let context_task = self.reload_context(cx);
|
||||
let window_handle = window.window_handle();
|
||||
|
||||
cx.spawn(async move |_this, cx| {
|
||||
@@ -312,31 +311,11 @@ impl MessageEditor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn wait_for_summaries(&mut self, cx: &mut Context<Self>) -> Task<()> {
|
||||
let context_store = self.context_store.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Some(wait_for_summaries) = context_store
|
||||
.update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
|
||||
.ok()
|
||||
{
|
||||
this.update(cx, |this, cx| {
|
||||
this.waiting_for_summaries_to_send = true;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
||||
wait_for_summaries.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.waiting_for_summaries_to_send = false;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_editing(cx);
|
||||
});
|
||||
|
||||
let cancelled = self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_last_completion(Some(window.window_handle()), cx)
|
||||
});
|
||||
@@ -425,14 +404,13 @@ impl MessageEditor {
|
||||
return None;
|
||||
}
|
||||
|
||||
let model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.model.clone())?;
|
||||
if !model.supports_max_mode() {
|
||||
let thread = self.thread.read(cx);
|
||||
let model = thread.configured_model();
|
||||
if !model?.model.supports_max_mode() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let active_completion_mode = self.thread.read(cx).completion_mode();
|
||||
let active_completion_mode = thread.completion_mode();
|
||||
|
||||
Some(
|
||||
IconButton::new("max-mode", IconName::ZedMaxMode)
|
||||
@@ -459,24 +437,21 @@ impl MessageEditor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Div {
|
||||
let thread = self.thread.read(cx);
|
||||
let model = thread.configured_model();
|
||||
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let is_generating = thread.is_generating();
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
|
||||
let is_model_selected = self.is_model_selected(cx);
|
||||
let is_model_selected = model.is_some();
|
||||
let is_editor_empty = self.is_editor_empty(cx);
|
||||
|
||||
let model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.model.clone());
|
||||
|
||||
let incompatible_tools = model
|
||||
.as_ref()
|
||||
.map(|model| {
|
||||
self.incompatible_tools_state.update(cx, |state, cx| {
|
||||
state
|
||||
.incompatible_tools(model, cx)
|
||||
.incompatible_tools(&model.model, cx)
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
@@ -520,32 +495,34 @@ impl MessageEditor {
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(self.context_strip.clone())
|
||||
.child(
|
||||
IconButton::new("toggle-height", expand_icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
let expand_label = if is_editor_expanded {
|
||||
"Minimize Message Editor".to_string()
|
||||
} else {
|
||||
"Expand Message Editor".to_string()
|
||||
};
|
||||
.when(focus_handle.is_focused(window), |this| {
|
||||
this.child(
|
||||
IconButton::new("toggle-height", expand_icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
let expand_label = if is_editor_expanded {
|
||||
"Minimize Message Editor".to_string()
|
||||
} else {
|
||||
"Expand Message Editor".to_string()
|
||||
};
|
||||
|
||||
Tooltip::for_action_in(
|
||||
expand_label,
|
||||
&ExpandMessageEditor,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
|
||||
})),
|
||||
),
|
||||
Tooltip::for_action_in(
|
||||
expand_label,
|
||||
&ExpandMessageEditor,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -664,31 +641,31 @@ impl MessageEditor {
|
||||
})
|
||||
.when(!is_editor_empty, |parent| {
|
||||
parent.child(
|
||||
IconButton::new("send-message", IconName::Send)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(
|
||||
!is_model_selected
|
||||
|| self
|
||||
.waiting_for_summaries_to_send,
|
||||
)
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&Chat, window, cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Stop and Send New Message",
|
||||
&Chat,
|
||||
window,
|
||||
cx,
|
||||
IconButton::new(
|
||||
"send-message",
|
||||
IconName::Send,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(!is_model_selected)
|
||||
.on_click({
|
||||
let focus_handle =
|
||||
focus_handle.clone();
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&Chat, window, cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Stop and Send New Message",
|
||||
&Chat,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
parent.child(
|
||||
@@ -696,10 +673,7 @@ impl MessageEditor {
|
||||
.icon_color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(
|
||||
is_editor_empty
|
||||
|| !is_model_selected
|
||||
|| self
|
||||
.waiting_for_summaries_to_send,
|
||||
is_editor_empty || !is_model_selected,
|
||||
)
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
@@ -750,9 +724,12 @@ impl MessageEditor {
|
||||
let border_color = cx.theme().colors().border;
|
||||
let active_color = cx.theme().colors().element_selected;
|
||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||
|
||||
let is_edit_changes_expanded = self.edits_expanded;
|
||||
let is_generating = self.thread.read(cx).is_generating();
|
||||
|
||||
v_flex()
|
||||
.mt_1()
|
||||
.mx_2()
|
||||
.bg(bg_edit_files_disclosure)
|
||||
.border_1()
|
||||
@@ -787,25 +764,44 @@ impl MessageEditor {
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Label::new("Edits")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
.map(|this| {
|
||||
if is_generating {
|
||||
this.child(
|
||||
AnimatedLabel::new(format!(
|
||||
"Editing {} {}",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
Label::new("Edits")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new("•").size(LabelSize::XSmall).color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_buffers.len(),
|
||||
if changed_buffers.len() == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("review", "Review Changes")
|
||||
@@ -895,7 +891,7 @@ impl MessageEditor {
|
||||
.justify_between()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.hover(|style| style.bg(hover_color))
|
||||
.when(index + 1 < changed_buffers.len(), |parent| {
|
||||
.when(index < changed_buffers.len() - 1, |parent| {
|
||||
parent.border_color(border_color).border_b_1()
|
||||
})
|
||||
.child(
|
||||
@@ -911,9 +907,9 @@ impl MessageEditor {
|
||||
.gap_0p5()
|
||||
.children(name_label)
|
||||
.children(parent_label),
|
||||
) // TODO: show lines changed
|
||||
.child(Label::new("+").color(Color::Created))
|
||||
.child(Label::new("-").color(Color::Deleted)),
|
||||
), // TODO: Implement line diff
|
||||
// .child(Label::new("+").color(Color::Created))
|
||||
// .child(Label::new("-").color(Color::Deleted)),
|
||||
)
|
||||
.child(
|
||||
div().visible_on_hover("edited-code").child(
|
||||
@@ -1041,16 +1037,8 @@ impl MessageEditor {
|
||||
self.update_token_count_task.is_some()
|
||||
}
|
||||
|
||||
fn handle_message_changed(&mut self, cx: &mut Context<Self>) {
|
||||
self.message_or_context_changed(true, cx);
|
||||
}
|
||||
|
||||
fn start_context_load(&mut self, cx: &mut Context<Self>) -> Shared<Task<()>> {
|
||||
let summaries_task = self.wait_for_summaries(cx);
|
||||
fn reload_context(&mut self, cx: &mut Context<Self>) -> Task<Option<ContextLoadResult>> {
|
||||
let load_task = cx.spawn(async move |this, cx| {
|
||||
// Waits for detailed summaries before `load_context`, as it directly reads these from
|
||||
// the thread. TODO: Would be cleaner to have context loading await on summarization.
|
||||
summaries_task.await;
|
||||
let Ok(load_task) = this.update(cx, |this, cx| {
|
||||
let new_context = this.context_store.read_with(cx, |context_store, cx| {
|
||||
context_store.new_context_for_thread(this.thread.read(cx))
|
||||
@@ -1062,32 +1050,31 @@ impl MessageEditor {
|
||||
let result = load_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.last_loaded_context = Some(result);
|
||||
this.context_load_task = None;
|
||||
this.load_context_task = None;
|
||||
this.message_or_context_changed(false, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
// Replace existing load task, if any, causing it to be cancelled.
|
||||
let load_task = load_task.shared();
|
||||
self.context_load_task = Some(load_task.clone());
|
||||
load_task
|
||||
}
|
||||
|
||||
fn load_context(&mut self, cx: &mut Context<Self>) -> Task<Option<ContextLoadResult>> {
|
||||
let context_load_task = self.start_context_load(cx);
|
||||
self.load_context_task = Some(load_task.clone());
|
||||
cx.spawn(async move |this, cx| {
|
||||
context_load_task.await;
|
||||
load_task.await;
|
||||
this.read_with(cx, |this, _cx| this.last_loaded_context.clone())
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_message_changed(&mut self, cx: &mut Context<Self>) {
|
||||
self.message_or_context_changed(true, cx);
|
||||
}
|
||||
|
||||
fn message_or_context_changed(&mut self, debounce: bool, cx: &mut Context<Self>) {
|
||||
cx.emit(MessageEditorEvent::Changed);
|
||||
self.update_token_count_task.take();
|
||||
|
||||
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
let Some(model) = self.thread.read(cx).configured_model() else {
|
||||
self.last_estimated_token_count.take();
|
||||
return;
|
||||
};
|
||||
@@ -1101,57 +1088,64 @@ impl MessageEditor {
|
||||
.await;
|
||||
}
|
||||
|
||||
let token_count = if let Some(task) = this.update(cx, |this, cx| {
|
||||
let loaded_context = this
|
||||
.last_loaded_context
|
||||
.as_ref()
|
||||
.map(|context_load_result| &context_load_result.loaded_context);
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
let token_count = if let Some(task) = this
|
||||
.update(cx, |this, cx| {
|
||||
let loaded_context = this
|
||||
.last_loaded_context
|
||||
.as_ref()
|
||||
.map(|context_load_result| &context_load_result.loaded_context);
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
|
||||
if message_text.is_empty()
|
||||
&& loaded_context.map_or(true, |loaded_context| loaded_context.is_empty())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if message_text.is_empty()
|
||||
&& loaded_context.map_or(true, |loaded_context| loaded_context.is_empty())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
if let Some(loaded_context) = loaded_context {
|
||||
loaded_context.add_to_request_message(&mut request_message);
|
||||
}
|
||||
if let Some(loaded_context) = loaded_context {
|
||||
loaded_context.add_to_request_message(&mut request_message);
|
||||
}
|
||||
|
||||
if !message_text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(message_text));
|
||||
}
|
||||
if !message_text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(message_text));
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
Some(default_model.model.count_tokens(request, cx))
|
||||
})? {
|
||||
task.await?
|
||||
Some(model.model.count_tokens(request, cx))
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
task.await.log_err()
|
||||
} else {
|
||||
0
|
||||
Some(0)
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(MessageEditorEvent::EstimatedTokenCount);
|
||||
if let Some(token_count) = token_count {
|
||||
this.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(MessageEditorEvent::EstimatedTokenCount);
|
||||
}
|
||||
this.update_token_count_task.take();
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1172,8 +1166,11 @@ impl Focusable for MessageEditor {
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let thread = self.thread.read(cx);
|
||||
let total_token_usage = thread.total_token_usage(cx);
|
||||
let token_usage_ratio = total_token_usage.ratio();
|
||||
let token_usage_ratio = thread
|
||||
.total_token_usage()
|
||||
.map_or(TokenUsageRatio::Normal, |total_token_usage| {
|
||||
total_token_usage.ratio()
|
||||
});
|
||||
|
||||
let action_log = self.thread.read(cx).action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
@@ -1183,41 +1180,6 @@ impl Render for MessageEditor {
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.when(self.waiting_for_summaries_to_send, |parent| {
|
||||
parent.child(
|
||||
h_flex().py_3().w_full().justify_center().child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.px_2()
|
||||
.py_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(gpui::Transformation::rotate(
|
||||
gpui::percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Summarizing context…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(changed_buffers.len() > 0, |parent| {
|
||||
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
|
||||
})
|
||||
|
||||
@@ -68,30 +68,41 @@ impl ProfileSelector {
|
||||
|
||||
menu = menu.header("Profiles");
|
||||
for (profile_id, profile) in self.profiles.clone() {
|
||||
menu = menu.toggleable_entry(
|
||||
profile.name.clone(),
|
||||
profile_id == settings.default_profile,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
let documentation = match profile.name.to_lowercase().as_str() {
|
||||
"write" => Some("Get help to write anything."),
|
||||
"ask" => Some("Chat about your codebase."),
|
||||
"manual" => Some("Chat about anything; no tools."),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile_by_id(profile_id.clone(), cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
);
|
||||
let entry = ContextMenuEntry::new(profile.name.clone())
|
||||
.toggleable(icon_position, profile_id == settings.default_profile);
|
||||
|
||||
let entry = if let Some(doc_text) = documentation {
|
||||
entry.documentation_aside(move |_| Label::new(doc_text).into_any_element())
|
||||
} else {
|
||||
entry
|
||||
};
|
||||
|
||||
menu = menu.item(entry.handler({
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
let profile_id = profile_id.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
|
||||
thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile_by_id(profile_id.clone(), cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
@@ -141,6 +152,7 @@ impl Render for ProfileSelector {
|
||||
|
||||
let this = cx.entity().clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
PopoverMenu::new("profile-selector")
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
@@ -183,7 +195,7 @@ impl Render for ProfileSelector {
|
||||
)
|
||||
.tooltip(Tooltip::text("The current model does not support tools."))
|
||||
})
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.anchor(gpui::Corner::BottomRight)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,18 @@ use futures::future::Shared;
|
||||
use futures::{FutureExt, StreamExt as _};
|
||||
use git::repository::DiffType;
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
|
||||
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, StopReason,
|
||||
TokenUsage,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, SelectedModel,
|
||||
StopReason, TokenUsage,
|
||||
};
|
||||
use postage::stream::Stream as _;
|
||||
use project::Project;
|
||||
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
|
||||
use prompt_store::{ModelContext, PromptBuilder};
|
||||
@@ -36,10 +38,11 @@ use util::{ResultExt as _, TryFutureExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionMode;
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{AgentContext, ContextLoadResult, LoadedContext};
|
||||
use crate::thread_store::{
|
||||
SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult,
|
||||
SerializedToolUse, SharedProjectContext,
|
||||
SerializedLanguageModel, SerializedMessage, SerializedMessageSegment, SerializedThread,
|
||||
SerializedToolResult, SerializedToolUse, SharedProjectContext,
|
||||
};
|
||||
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState};
|
||||
|
||||
@@ -243,6 +246,16 @@ pub enum DetailedSummaryState {
|
||||
},
|
||||
}
|
||||
|
||||
impl DetailedSummaryState {
|
||||
fn text(&self) -> Option<SharedString> {
|
||||
if let Self::Generated { text, .. } = self {
|
||||
Some(text.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TotalTokenUsage {
|
||||
pub total: usize,
|
||||
@@ -259,7 +272,11 @@ impl TotalTokenUsage {
|
||||
#[cfg(not(debug_assertions))]
|
||||
let warning_threshold: f32 = 0.8;
|
||||
|
||||
if self.total >= self.max {
|
||||
// When the maximum is unknown because there is no selected model,
|
||||
// avoid showing the token limit warning.
|
||||
if self.max == 0 {
|
||||
TokenUsageRatio::Normal
|
||||
} else if self.total >= self.max {
|
||||
TokenUsageRatio::Exceeded
|
||||
} else if self.total as f32 / self.max as f32 >= warning_threshold {
|
||||
TokenUsageRatio::Warning
|
||||
@@ -290,7 +307,9 @@ pub struct Thread {
|
||||
updated_at: DateTime<Utc>,
|
||||
summary: Option<SharedString>,
|
||||
pending_summary: Task<Option<()>>,
|
||||
detailed_summary_state: DetailedSummaryState,
|
||||
detailed_summary_task: Task<Option<()>>,
|
||||
detailed_summary_tx: postage::watch::Sender<DetailedSummaryState>,
|
||||
detailed_summary_rx: postage::watch::Receiver<DetailedSummaryState>,
|
||||
completion_mode: Option<CompletionMode>,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
@@ -317,6 +336,7 @@ pub struct Thread {
|
||||
Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>,
|
||||
>,
|
||||
remaining_turns: u32,
|
||||
configured_model: Option<ConfiguredModel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -335,12 +355,17 @@ impl Thread {
|
||||
system_prompt: SharedProjectContext,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let (detailed_summary_tx, detailed_summary_rx) = postage::watch::channel();
|
||||
let configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
|
||||
Self {
|
||||
id: ThreadId::new(),
|
||||
updated_at: Utc::now(),
|
||||
summary: None,
|
||||
pending_summary: Task::ready(None),
|
||||
detailed_summary_state: DetailedSummaryState::NotGenerated,
|
||||
detailed_summary_task: Task::ready(None),
|
||||
detailed_summary_tx,
|
||||
detailed_summary_rx,
|
||||
completion_mode: None,
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
@@ -370,6 +395,7 @@ impl Thread {
|
||||
last_auto_capture_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,13 +416,30 @@ impl Thread {
|
||||
.unwrap_or(0),
|
||||
);
|
||||
let tool_use = ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages);
|
||||
let (detailed_summary_tx, detailed_summary_rx) =
|
||||
postage::watch::channel_with(serialized.detailed_summary_state);
|
||||
|
||||
let configured_model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
serialized
|
||||
.model
|
||||
.and_then(|model| {
|
||||
let model = SelectedModel {
|
||||
provider: model.provider.clone().into(),
|
||||
model: model.model.clone().into(),
|
||||
};
|
||||
registry.select_model(&model, cx)
|
||||
})
|
||||
.or_else(|| registry.default_model())
|
||||
});
|
||||
|
||||
Self {
|
||||
id,
|
||||
updated_at: serialized.updated_at,
|
||||
summary: Some(serialized.summary),
|
||||
pending_summary: Task::ready(None),
|
||||
detailed_summary_state: serialized.detailed_summary_state,
|
||||
detailed_summary_task: Task::ready(None),
|
||||
detailed_summary_tx,
|
||||
detailed_summary_rx,
|
||||
completion_mode: None,
|
||||
messages: serialized
|
||||
.messages
|
||||
@@ -446,6 +489,7 @@ impl Thread {
|
||||
last_auto_capture_at: None,
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,6 +529,22 @@ impl Thread {
|
||||
self.project_context.clone()
|
||||
}
|
||||
|
||||
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
|
||||
if self.configured_model.is_none() {
|
||||
self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
}
|
||||
self.configured_model.clone()
|
||||
}
|
||||
|
||||
pub fn configured_model(&self) -> Option<ConfiguredModel> {
|
||||
self.configured_model.clone()
|
||||
}
|
||||
|
||||
pub fn set_configured_model(&mut self, model: Option<ConfiguredModel>, cx: &mut Context<Self>) {
|
||||
self.configured_model = model;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
||||
|
||||
pub fn summary_or_default(&self) -> SharedString {
|
||||
@@ -509,19 +569,6 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn latest_detailed_summary_or_text(&self) -> SharedString {
|
||||
self.latest_detailed_summary()
|
||||
.unwrap_or_else(|| self.text().into())
|
||||
}
|
||||
|
||||
fn latest_detailed_summary(&self) -> Option<SharedString> {
|
||||
if let DetailedSummaryState::Generated { text, .. } = &self.detailed_summary_state {
|
||||
Some(text.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn completion_mode(&self) -> Option<CompletionMode> {
|
||||
self.completion_mode
|
||||
}
|
||||
@@ -941,8 +988,15 @@ impl Thread {
|
||||
initial_project_snapshot,
|
||||
cumulative_token_usage: this.cumulative_token_usage,
|
||||
request_token_usage: this.request_token_usage.clone(),
|
||||
detailed_summary_state: this.detailed_summary_state.clone(),
|
||||
detailed_summary_state: this.detailed_summary_rx.borrow().clone(),
|
||||
exceeded_window_error: this.exceeded_window_error.clone(),
|
||||
model: this
|
||||
.configured_model
|
||||
.as_ref()
|
||||
.map(|model| SerializedLanguageModel {
|
||||
provider: model.provider.id().0.to_string(),
|
||||
model: model.model.id().0.to_string(),
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1540,25 +1594,34 @@ impl Thread {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn generate_detailed_summary(&mut self, cx: &mut Context<Self>) -> Option<Task<()>> {
|
||||
let last_message_id = self.messages.last().map(|message| message.id)?;
|
||||
pub fn start_generating_detailed_summary_if_needed(
|
||||
&mut self,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(last_message_id) = self.messages.last().map(|message| message.id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match &self.detailed_summary_state {
|
||||
match &*self.detailed_summary_rx.borrow() {
|
||||
DetailedSummaryState::Generating { message_id, .. }
|
||||
| DetailedSummaryState::Generated { message_id, .. }
|
||||
if *message_id == last_message_id =>
|
||||
{
|
||||
// Already up-to-date
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let ConfiguredModel { model, provider } =
|
||||
LanguageModelRegistry::read_global(cx).thread_summary_model()?;
|
||||
let Some(ConfiguredModel { model, provider }) =
|
||||
LanguageModelRegistry::read_global(cx).thread_summary_model()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !provider.is_authenticated(cx) {
|
||||
return None;
|
||||
return;
|
||||
}
|
||||
|
||||
let added_user_message = "Generate a detailed summary of this conversation. Include:\n\
|
||||
@@ -1570,16 +1633,24 @@ impl Thread {
|
||||
|
||||
let request = self.to_summarize_request(added_user_message.into());
|
||||
|
||||
let task = cx.spawn(async move |thread, cx| {
|
||||
*self.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generating {
|
||||
message_id: last_message_id,
|
||||
};
|
||||
|
||||
// Replace the detailed summarization task if there is one, cancelling it. It would probably
|
||||
// be better to allow the old task to complete, but this would require logic for choosing
|
||||
// which result to prefer (the old task could complete after the new one, resulting in a
|
||||
// stale summary).
|
||||
self.detailed_summary_task = cx.spawn(async move |thread, cx| {
|
||||
let stream = model.stream_completion_text(request, &cx);
|
||||
let Some(mut messages) = stream.await.log_err() else {
|
||||
thread
|
||||
.update(cx, |this, _cx| {
|
||||
this.detailed_summary_state = DetailedSummaryState::NotGenerated;
|
||||
.update(cx, |thread, _cx| {
|
||||
*thread.detailed_summary_tx.borrow_mut() =
|
||||
DetailedSummaryState::NotGenerated;
|
||||
})
|
||||
.log_err();
|
||||
|
||||
return;
|
||||
.ok()?;
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut new_detailed_summary = String::new();
|
||||
@@ -1591,25 +1662,56 @@ impl Thread {
|
||||
}
|
||||
|
||||
thread
|
||||
.update(cx, |this, _cx| {
|
||||
this.detailed_summary_state = DetailedSummaryState::Generated {
|
||||
.update(cx, |thread, _cx| {
|
||||
*thread.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generated {
|
||||
text: new_detailed_summary.into(),
|
||||
message_id: last_message_id,
|
||||
};
|
||||
})
|
||||
.log_err();
|
||||
.ok()?;
|
||||
|
||||
// Save thread so its summary can be reused later
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
if let Ok(Ok(save_task)) = cx.update(|cx| {
|
||||
thread_store
|
||||
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
|
||||
}) {
|
||||
save_task.await.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
self.detailed_summary_state = DetailedSummaryState::Generating {
|
||||
message_id: last_message_id,
|
||||
};
|
||||
pub async fn wait_for_detailed_summary_or_text(
|
||||
this: &Entity<Self>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<SharedString> {
|
||||
let mut detailed_summary_rx = this
|
||||
.read_with(cx, |this, _cx| this.detailed_summary_rx.clone())
|
||||
.ok()?;
|
||||
loop {
|
||||
match detailed_summary_rx.recv().await? {
|
||||
DetailedSummaryState::Generating { .. } => {}
|
||||
DetailedSummaryState::NotGenerated => {
|
||||
return this.read_with(cx, |this, _cx| this.text().into()).ok();
|
||||
}
|
||||
DetailedSummaryState::Generated { text, .. } => return Some(text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(task)
|
||||
pub fn latest_detailed_summary_or_text(&self) -> SharedString {
|
||||
self.detailed_summary_rx
|
||||
.borrow()
|
||||
.text()
|
||||
.unwrap_or_else(|| self.text().into())
|
||||
}
|
||||
|
||||
pub fn is_generating_detailed_summary(&self) -> bool {
|
||||
matches!(
|
||||
self.detailed_summary_state,
|
||||
&*self.detailed_summary_rx.borrow(),
|
||||
DetailedSummaryState::Generating { .. }
|
||||
)
|
||||
}
|
||||
@@ -1676,7 +1778,7 @@ impl Thread {
|
||||
tool_use_id.clone(),
|
||||
tool_name,
|
||||
Err(anyhow!("Error parsing input JSON: {error}")),
|
||||
cx,
|
||||
self.configured_model.as_ref(),
|
||||
);
|
||||
let ui_text = if let Some(pending_tool_use) = &pending_tool_use {
|
||||
pending_tool_use.ui_text.clone()
|
||||
@@ -1751,7 +1853,7 @@ impl Thread {
|
||||
tool_use_id.clone(),
|
||||
tool_name,
|
||||
output,
|
||||
cx,
|
||||
thread.configured_model.as_ref(),
|
||||
);
|
||||
thread.tool_finished(tool_use_id, pending_tool_use, false, window, cx);
|
||||
})
|
||||
@@ -1769,10 +1871,9 @@ impl Thread {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.all_tools_finished() {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
|
||||
if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() {
|
||||
if !canceled {
|
||||
self.send_to_model(model, window, cx);
|
||||
self.send_to_model(model.clone(), window, cx);
|
||||
}
|
||||
self.auto_capture_telemetry(cx);
|
||||
}
|
||||
@@ -1809,6 +1910,14 @@ impl Thread {
|
||||
canceled
|
||||
}
|
||||
|
||||
/// Signals that any in-progress editing should be canceled.
|
||||
///
|
||||
/// This method is used to notify listeners (like ActiveThread) that
|
||||
/// they should cancel any editing operations.
|
||||
pub fn cancel_editing(&mut self, cx: &mut Context<Self>) {
|
||||
cx.emit(ThreadEvent::CancelEditing);
|
||||
}
|
||||
|
||||
pub fn feedback(&self) -> Option<ThreadFeedback> {
|
||||
self.feedback
|
||||
}
|
||||
@@ -2009,7 +2118,7 @@ impl Thread {
|
||||
};
|
||||
|
||||
let remote_url = backend.remote_url("origin");
|
||||
let head_sha = backend.head_sha();
|
||||
let head_sha = backend.head_sha().await;
|
||||
let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
|
||||
|
||||
GitState {
|
||||
@@ -2189,8 +2298,8 @@ impl Thread {
|
||||
self.cumulative_token_usage
|
||||
}
|
||||
|
||||
pub fn token_usage_up_to_message(&self, message_id: MessageId, cx: &App) -> TotalTokenUsage {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
pub fn token_usage_up_to_message(&self, message_id: MessageId) -> TotalTokenUsage {
|
||||
let Some(model) = self.configured_model.as_ref() else {
|
||||
return TotalTokenUsage::default();
|
||||
};
|
||||
|
||||
@@ -2218,20 +2327,17 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(model) = model_registry.default_model() else {
|
||||
return TotalTokenUsage::default();
|
||||
};
|
||||
pub fn total_token_usage(&self) -> Option<TotalTokenUsage> {
|
||||
let model = self.configured_model.as_ref()?;
|
||||
|
||||
let max = model.model.max_token_count();
|
||||
|
||||
if let Some(exceeded_error) = &self.exceeded_window_error {
|
||||
if model.model.id() == exceeded_error.model_id {
|
||||
return TotalTokenUsage {
|
||||
return Some(TotalTokenUsage {
|
||||
total: exceeded_error.token_count,
|
||||
max,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2240,7 +2346,7 @@ impl Thread {
|
||||
.unwrap_or_default()
|
||||
.total_tokens() as usize;
|
||||
|
||||
TotalTokenUsage { total, max }
|
||||
Some(TotalTokenUsage { total, max })
|
||||
}
|
||||
|
||||
fn token_usage_at_last_message(&self) -> Option<TokenUsage> {
|
||||
@@ -2271,8 +2377,12 @@ impl Thread {
|
||||
"Permission to run tool action denied by user"
|
||||
));
|
||||
|
||||
self.tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), tool_name, err, cx);
|
||||
self.tool_use.insert_tool_output(
|
||||
tool_use_id.clone(),
|
||||
tool_name,
|
||||
err,
|
||||
self.configured_model.as_ref(),
|
||||
);
|
||||
self.tool_finished(tool_use_id.clone(), None, true, window, cx);
|
||||
}
|
||||
}
|
||||
@@ -2327,6 +2437,7 @@ pub enum ThreadEvent {
|
||||
},
|
||||
CheckpointChanged,
|
||||
ToolConfirmationNeeded,
|
||||
CancelEditing,
|
||||
}
|
||||
|
||||
impl EventEmitter<ThreadEvent> for Thread {}
|
||||
@@ -2703,6 +2814,7 @@ fn main() {{
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
language_model::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
ContextServerSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
|
||||
@@ -270,9 +270,9 @@ impl ThreadHistory {
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(entry) = self.get_match(self.selected_index) {
|
||||
let task_result = match entry {
|
||||
HistoryEntry::Thread(thread) => self
|
||||
.assistant_panel
|
||||
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx)),
|
||||
HistoryEntry::Thread(thread) => self.assistant_panel.update(cx, move |this, cx| {
|
||||
this.open_thread_by_id(&thread.id, window, cx)
|
||||
}),
|
||||
HistoryEntry::Context(context) => {
|
||||
self.assistant_panel.update(cx, move |this, cx| {
|
||||
this.open_saved_prompt_editor(context.path.clone(), window, cx)
|
||||
@@ -525,7 +525,8 @@ impl RenderOnce for PastThread {
|
||||
move |_event, window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread(&id, window, cx).detach_and_log_err(cx);
|
||||
this.open_thread_by_id(&id, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -640,6 +640,14 @@ pub struct SerializedThread {
|
||||
pub detailed_summary_state: DetailedSummaryState,
|
||||
#[serde(default)]
|
||||
pub exceeded_window_error: Option<ExceededWindowError>,
|
||||
#[serde(default)]
|
||||
pub model: Option<SerializedLanguageModel>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SerializedLanguageModel {
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
impl SerializedThread {
|
||||
@@ -774,6 +782,7 @@ impl LegacySerializedThread {
|
||||
request_token_usage: Vec::new(),
|
||||
detailed_summary_state: DetailedSummaryState::default(),
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use futures::FutureExt as _;
|
||||
use futures::future::Shared;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
|
||||
ConfiguredModel, LanguageModel, LanguageModelRequestMessage, LanguageModelToolResult,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
|
||||
};
|
||||
use ui::IconName;
|
||||
@@ -353,7 +353,7 @@ impl ToolUseState {
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
output: Result<String>,
|
||||
cx: &App,
|
||||
configured_model: Option<&ConfiguredModel>,
|
||||
) -> Option<PendingToolUse> {
|
||||
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
|
||||
|
||||
@@ -373,13 +373,10 @@ impl ToolUseState {
|
||||
|
||||
match output {
|
||||
Ok(tool_result) => {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
|
||||
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
|
||||
|
||||
// Protect from clearly large output
|
||||
let tool_output_limit = model_registry
|
||||
.default_model()
|
||||
let tool_output_limit = configured_model
|
||||
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
|
||||
@@ -491,7 +491,7 @@ impl AddedContext {
|
||||
let thread = handle.thread.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
let text = thread.read(cx).latest_detailed_summary_or_text();
|
||||
text_hover_view(text.clone(), cx).into()
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Thread(handle),
|
||||
@@ -509,7 +509,7 @@ impl AddedContext {
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
text_hover_view(text.clone(), cx).into()
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Thread(context.handle.clone()),
|
||||
@@ -554,7 +554,7 @@ impl AddedContext {
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
text_hover_view(text.clone(), cx).into()
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Rules(context.handle.clone()),
|
||||
@@ -670,18 +670,6 @@ impl ContextFileExcerpt {
|
||||
}
|
||||
}
|
||||
|
||||
fn text_hover_view(content: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
|
||||
ContextPillHover::new(cx, move |_, _| {
|
||||
div()
|
||||
.id("context-pill-hover-contents")
|
||||
.overflow_scroll()
|
||||
.max_w_128()
|
||||
.max_h_96()
|
||||
.child(content.clone())
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
struct ContextPillHover {
|
||||
render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||
}
|
||||
@@ -695,6 +683,18 @@ impl ContextPillHover {
|
||||
render_hover: Box::new(render_hover),
|
||||
})
|
||||
}
|
||||
|
||||
fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
|
||||
Self::new(cx, move |_, _| {
|
||||
div()
|
||||
.id("context-pill-hover-contents")
|
||||
.overflow_scroll()
|
||||
.max_w_128()
|
||||
.max_h_96()
|
||||
.child(content.clone())
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContextPillHover {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
mod supported_countries;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -11,8 +9,6 @@ use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumIter, EnumString};
|
||||
use thiserror::Error;
|
||||
|
||||
pub use supported_countries::*;
|
||||
|
||||
pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
|
||||
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// Returns whether the given country code is supported by Anthropic.
|
||||
///
|
||||
/// <https://www.anthropic.com/supported-countries>
|
||||
pub fn is_supported_country(country_code: &str) -> bool {
|
||||
SUPPORTED_COUNTRIES.contains(&country_code)
|
||||
}
|
||||
|
||||
/// The list of country codes supported by Anthropic.
|
||||
///
|
||||
/// https://www.anthropic.com/supported-countries
|
||||
static SUPPORTED_COUNTRIES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||
vec![
|
||||
"AL", // Albania
|
||||
"DZ", // Algeria
|
||||
"AS", // American Samoa (US)
|
||||
"AD", // Andorra
|
||||
"AO", // Angola
|
||||
"AI", // Anguilla (UK)
|
||||
"AG", // Antigua and Barbuda
|
||||
"AR", // Argentina
|
||||
"AM", // Armenia
|
||||
"AU", // Australia
|
||||
"AT", // Austria
|
||||
"AZ", // Azerbaijan
|
||||
"BS", // Bahamas
|
||||
"BH", // Bahrain
|
||||
"BD", // Bangladesh
|
||||
"BB", // Barbados
|
||||
"BE", // Belgium
|
||||
"BZ", // Belize
|
||||
"BJ", // Benin
|
||||
"BM", // Bermuda (UK)
|
||||
"BT", // Bhutan
|
||||
"BO", // Bolivia
|
||||
"BA", // Bosnia and Herzegovina
|
||||
"BW", // Botswana
|
||||
"BR", // Brazil
|
||||
"IO", // British Indian Ocean Territory (UK)
|
||||
"BN", // Brunei
|
||||
"BG", // Bulgaria
|
||||
"BF", // Burkina Faso
|
||||
"BI", // Burundi
|
||||
"CV", // Cabo Verde
|
||||
"KH", // Cambodia
|
||||
"CM", // Cameroon
|
||||
"CA", // Canada
|
||||
"KY", // Cayman Islands (UK)
|
||||
"TD", // Chad
|
||||
"CL", // Chile
|
||||
"CX", // Christmas Island (AU)
|
||||
"CC", // Cocos (Keeling) Islands (AU)
|
||||
"CO", // Colombia
|
||||
"KM", // Comoros
|
||||
"CG", // Congo (Brazzaville)
|
||||
"CK", // Cook Islands (NZ)
|
||||
"CR", // Costa Rica
|
||||
"CI", // Côte d'Ivoire
|
||||
"HR", // Croatia
|
||||
"CY", // Cyprus
|
||||
"CZ", // Czechia (Czech Republic)
|
||||
"DK", // Denmark
|
||||
"DJ", // Djibouti
|
||||
"DM", // Dominica
|
||||
"DO", // Dominican Republic
|
||||
"EC", // Ecuador
|
||||
"EG", // Egypt
|
||||
"SV", // El Salvador
|
||||
"GQ", // Equatorial Guinea
|
||||
"EE", // Estonia
|
||||
"SZ", // Eswatini
|
||||
"FK", // Falkland Islands (UK)
|
||||
"FJ", // Fiji
|
||||
"FI", // Finland
|
||||
"FR", // France
|
||||
"GF", // French Guiana (FR)
|
||||
"PF", // French Polynesia (FR)
|
||||
"TF", // French Southern Territories
|
||||
"GA", // Gabon
|
||||
"GM", // Gambia
|
||||
"GE", // Georgia
|
||||
"DE", // Germany
|
||||
"GH", // Ghana
|
||||
"GI", // Gibraltar (UK)
|
||||
"GR", // Greece
|
||||
"GD", // Grenada
|
||||
"GT", // Guatemala
|
||||
"GU", // Guam (US)
|
||||
"GN", // Guinea
|
||||
"GW", // Guinea-Bissau
|
||||
"GY", // Guyana
|
||||
"HT", // Haiti
|
||||
"HM", // Heard Island and McDonald Islands (AU)
|
||||
"HN", // Honduras
|
||||
"HU", // Hungary
|
||||
"IS", // Iceland
|
||||
"IN", // India
|
||||
"ID", // Indonesia
|
||||
"IQ", // Iraq
|
||||
"IE", // Ireland
|
||||
"IL", // Israel
|
||||
"IT", // Italy
|
||||
"JM", // Jamaica
|
||||
"JP", // Japan
|
||||
"JO", // Jordan
|
||||
"KZ", // Kazakhstan
|
||||
"KE", // Kenya
|
||||
"KI", // Kiribati
|
||||
"KW", // Kuwait
|
||||
"KG", // Kyrgyzstan
|
||||
"LA", // Laos
|
||||
"LV", // Latvia
|
||||
"LB", // Lebanon
|
||||
"LS", // Lesotho
|
||||
"LR", // Liberia
|
||||
"LI", // Liechtenstein
|
||||
"LT", // Lithuania
|
||||
"LU", // Luxembourg
|
||||
"MG", // Madagascar
|
||||
"MW", // Malawi
|
||||
"MY", // Malaysia
|
||||
"MV", // Maldives
|
||||
"MT", // Malta
|
||||
"MH", // Marshall Islands
|
||||
"MR", // Mauritania
|
||||
"MU", // Mauritius
|
||||
"MX", // Mexico
|
||||
"FM", // Micronesia
|
||||
"MD", // Moldova
|
||||
"MC", // Monaco
|
||||
"MN", // Mongolia
|
||||
"MS", // Montserrat (UK)
|
||||
"ME", // Montenegro
|
||||
"MA", // Morocco
|
||||
"MZ", // Mozambique
|
||||
"NA", // Namibia
|
||||
"NR", // Nauru
|
||||
"NP", // Nepal
|
||||
"NL", // Netherlands
|
||||
"NZ", // New Zealand
|
||||
"NE", // Niger
|
||||
"NG", // Nigeria
|
||||
"NF", // Norfolk Island (AU)
|
||||
"MK", // North Macedonia
|
||||
"MI", // Northern Mariana Islands (UK)
|
||||
"NO", // Norway
|
||||
"NU", // Niue (NZ)
|
||||
"OM", // Oman
|
||||
"PK", // Pakistan
|
||||
"PW", // Palau
|
||||
"PS", // Palestine
|
||||
"PA", // Panama
|
||||
"PG", // Papua New Guinea
|
||||
"PY", // Paraguay
|
||||
"PE", // Peru
|
||||
"PH", // Philippines
|
||||
"PN", // Pitcairn (UK)
|
||||
"PL", // Poland
|
||||
"PT", // Portugal
|
||||
"PR", // Puerto Rico (US)
|
||||
"QA", // Qatar
|
||||
"RO", // Romania
|
||||
"RW", // Rwanda
|
||||
"BL", // Saint Barthélemy (FR)
|
||||
"KN", // Saint Kitts and Nevis
|
||||
"LC", // Saint Lucia
|
||||
"MF", // Saint Martin (FR)
|
||||
"PM", // Saint Pierre and Miquelon (FR)
|
||||
"VC", // Saint Vincent and the Grenadines
|
||||
"WS", // Samoa
|
||||
"SM", // San Marino
|
||||
"ST", // São Tomé and Príncipe
|
||||
"SA", // Saudi Arabia
|
||||
"SN", // Senegal
|
||||
"RS", // Serbia
|
||||
"SC", // Seychelles
|
||||
"SH", // Saint Helena, Ascension and Tristan da Cunha (UK)
|
||||
"SL", // Sierra Leone
|
||||
"SG", // Singapore
|
||||
"SK", // Slovakia
|
||||
"SI", // Slovenia
|
||||
"SB", // Solomon Islands
|
||||
"ZA", // South Africa
|
||||
"KR", // South Korea
|
||||
"ES", // Spain
|
||||
"LK", // Sri Lanka
|
||||
"SR", // Suriname
|
||||
"SE", // Sweden
|
||||
"CH", // Switzerland
|
||||
"TW", // Taiwan
|
||||
"TJ", // Tajikistan
|
||||
"TZ", // Tanzania
|
||||
"TH", // Thailand
|
||||
"TL", // Timor-Leste
|
||||
"TG", // Togo
|
||||
"TK", // Tokelau (NZ)
|
||||
"TO", // Tonga
|
||||
"TT", // Trinidad and Tobago
|
||||
"TN", // Tunisia
|
||||
"TR", // Türkiye (Turkey)
|
||||
"TM", // Turkmenistan
|
||||
"TC", // Turks and Caicos Islands (UK)
|
||||
"TV", // Tuvalu
|
||||
"UG", // Uganda
|
||||
"UA", // Ukraine (except Crimea, Donetsk, and Luhansk regions)
|
||||
"AE", // United Arab Emirates
|
||||
"GB", // United Kingdom
|
||||
"UM", // United States Minor Outlying Islands (US)
|
||||
"US", // United States of America
|
||||
"UY", // Uruguay
|
||||
"UZ", // Uzbekistan
|
||||
"VU", // Vanuatu
|
||||
"VA", // Vatican City
|
||||
"VN", // Vietnam
|
||||
"VI", // Virgin Islands (US)
|
||||
"VG", // Virgin Islands (UK)
|
||||
"WF", // Wallis and Futuna (FR)
|
||||
"ZM", // Zambia
|
||||
"ZW", // Zimbabwe
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
});
|
||||
@@ -193,7 +193,7 @@ impl Focusable for ConfigurationView {
|
||||
impl Item for ConfigurationView {
|
||||
type Event = ConfigurationViewEvent;
|
||||
|
||||
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
|
||||
Some("Configuration".into())
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Configuration".into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ use settings::{Settings, update_settings_file};
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
|
||||
@@ -1080,7 +1081,7 @@ impl AssistantPanel {
|
||||
|
||||
pub fn open_saved_context(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
@@ -1391,7 +1392,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
fn open_saved_context(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<Result<()>> {
|
||||
|
||||
@@ -37,7 +37,7 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role, report_assistant_event,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, LspAction, ProjectTransaction};
|
||||
@@ -1226,7 +1226,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<InlineAssist>(
|
||||
row_range,
|
||||
cx.theme().status().info_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1291,7 +1291,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
cx.theme().status().deleted_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
@@ -1759,6 +1759,7 @@ impl PromptEditor {
|
||||
language_model_selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
@@ -1766,7 +1767,6 @@ impl PromptEditor {
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -3024,7 +3024,7 @@ impl CodegenAlternative {
|
||||
}
|
||||
}
|
||||
|
||||
let http_client = cx.http_client().clone();
|
||||
let http_client = cx.http_client();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let language_name = {
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
|
||||
@@ -44,4 +44,6 @@ impl Settings for SlashCommandSettings {
|
||||
.chain(sources.server),
|
||||
)
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
Role, report_assistant_event,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use std::{
|
||||
@@ -749,6 +749,7 @@ impl PromptEditor {
|
||||
language_model_selector: cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
@@ -756,7 +757,6 @@ impl PromptEditor {
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ use std::{
|
||||
fmt::Debug,
|
||||
iter, mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -46,7 +46,7 @@ use ui::IconName;
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct ContextId(String);
|
||||
|
||||
impl ContextId {
|
||||
@@ -648,7 +648,7 @@ pub struct AssistantContext {
|
||||
pending_token_count: Task<Option<()>>,
|
||||
pending_save: Task<Result<()>>,
|
||||
pending_cache_warming_task: Task<Option<()>>,
|
||||
path: Option<PathBuf>,
|
||||
path: Option<Arc<Path>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
@@ -839,7 +839,7 @@ impl AssistantContext {
|
||||
|
||||
pub fn deserialize(
|
||||
saved_context: SavedContext,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
@@ -1147,8 +1147,8 @@ impl AssistantContext {
|
||||
self.prompt_builder.clone()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.path.as_deref()
|
||||
pub fn path(&self) -> Option<&Arc<Path>> {
|
||||
self.path.as_ref()
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Option<&ContextSummary> {
|
||||
@@ -3181,7 +3181,7 @@ impl AssistantContext {
|
||||
fs.atomic_write(new_path.clone(), serde_json::to_string(&context).unwrap())
|
||||
.await?;
|
||||
if let Some(old_path) = old_path {
|
||||
if new_path != old_path {
|
||||
if new_path.as_path() != old_path.as_ref() {
|
||||
fs.remove_file(
|
||||
&old_path,
|
||||
RemoveOptions {
|
||||
@@ -3193,7 +3193,7 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
this.update(cx, |this, _| this.path = Some(new_path))?;
|
||||
this.update(cx, |this, _| this.path = Some(new_path.into()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -3589,6 +3589,6 @@ impl SavedContextV0_1_0 {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SavedContextMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub path: Arc<Path>,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
@@ -959,7 +959,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
let deserialized_context = cx.new(|cx| {
|
||||
AssistantContext::deserialize(
|
||||
serialized_context,
|
||||
Default::default(),
|
||||
Path::new("").into(),
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
@@ -1120,7 +1120,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
let deserialized_context = cx.new(|cx| {
|
||||
AssistantContext::deserialize(
|
||||
serialized_context,
|
||||
Default::default(),
|
||||
Path::new("").into(),
|
||||
registry.clone(),
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
|
||||
@@ -39,7 +39,7 @@ use language_model::{
|
||||
Role,
|
||||
};
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType, ToggleModelSelector,
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::Picker;
|
||||
@@ -48,7 +48,14 @@ use project::{Project, Worktree};
|
||||
use rope::Point;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use std::{any::TypeId, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
cmp,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use text::SelectionGoal;
|
||||
use ui::{
|
||||
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
|
||||
@@ -139,7 +146,7 @@ pub trait AssistantPanelDelegate {
|
||||
fn open_saved_context(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<Result<()>>;
|
||||
@@ -291,6 +298,7 @@ impl ContextEditor {
|
||||
dragged_file_worktrees: Vec::new(),
|
||||
language_model_selector: cx.new(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
@@ -298,7 +306,6 @@ impl ContextEditor {
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -3156,8 +3163,8 @@ impl Focusable for ContextEditor {
|
||||
impl Item for ContextEditor {
|
||||
type Event = editor::EditorEvent;
|
||||
|
||||
fn tab_content_text(&self, _window: &Window, cx: &App) -> Option<SharedString> {
|
||||
Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
|
||||
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
|
||||
util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
|
||||
@@ -3764,7 +3771,7 @@ pub fn make_lsp_adapter_delegate(
|
||||
let Some(worktree) = project.worktrees(cx).next() else {
|
||||
return Ok(None::<Arc<dyn LspAdapterDelegate>>);
|
||||
};
|
||||
let http_client = project.client().http_client().clone();
|
||||
let http_client = project.client().http_client();
|
||||
project.lsp_store().update(cx, |_, cx| {
|
||||
Ok(Some(LocalLspAdapterDelegate::new(
|
||||
project.languages().clone(),
|
||||
|
||||
@@ -108,8 +108,8 @@ impl EventEmitter<()> for ContextHistory {}
|
||||
impl Item for ContextHistory {
|
||||
type Event = ();
|
||||
|
||||
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
|
||||
Some("History".into())
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"History".into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,14 +20,7 @@ use prompt_store::PromptBuilder;
|
||||
use regex::Regex;
|
||||
use rpc::AnyProtoClient;
|
||||
use std::sync::LazyLock;
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
mem,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::{cmp::Reverse, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub(crate) fn init(client: &AnyProtoClient) {
|
||||
@@ -430,7 +423,7 @@ impl ContextStore {
|
||||
|
||||
pub fn open_local_context(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
cx: &Context<Self>,
|
||||
) -> Task<Result<Entity<AssistantContext>>> {
|
||||
if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
|
||||
@@ -478,7 +471,7 @@ impl ContextStore {
|
||||
|
||||
pub fn delete_local_context(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
@@ -501,7 +494,7 @@ impl ContextStore {
|
||||
!= Some(&path)
|
||||
});
|
||||
this.contexts_metadata
|
||||
.retain(|context| context.path != path);
|
||||
.retain(|context| context.path.as_ref() != path.as_ref());
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -511,7 +504,7 @@ impl ContextStore {
|
||||
fn loaded_context_for_path(&self, path: &Path, cx: &App) -> Option<Entity<AssistantContext>> {
|
||||
self.contexts.iter().find_map(|context| {
|
||||
let context = context.upgrade()?;
|
||||
if context.read(cx).path() == Some(path) {
|
||||
if context.read(cx).path().map(Arc::as_ref) == Some(path) {
|
||||
Some(context)
|
||||
} else {
|
||||
None
|
||||
@@ -794,7 +787,7 @@ impl ContextStore {
|
||||
{
|
||||
contexts.push(SavedContextMetadata {
|
||||
title: title.to_string(),
|
||||
path,
|
||||
path: path.into(),
|
||||
mtime: metadata.mtime.timestamp_for_user().into(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -112,13 +112,27 @@ impl AssistantSettings {
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
pub struct AssistantSettingsContent {
|
||||
#[serde(flatten)]
|
||||
pub inner: Option<AssistantSettingsContentInner>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AssistantSettingsContent {
|
||||
pub enum AssistantSettingsContentInner {
|
||||
Versioned(Box<VersionedAssistantSettingsContent>),
|
||||
Legacy(LegacyAssistantSettingsContent),
|
||||
}
|
||||
|
||||
impl AssistantSettingsContentInner {
|
||||
fn for_v2(content: AssistantSettingsContentV2) -> Self {
|
||||
AssistantSettingsContentInner::Versioned(Box::new(VersionedAssistantSettingsContent::V2(
|
||||
content,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for AssistantSettingsContent {
|
||||
fn schema_name() -> String {
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
@@ -133,26 +147,21 @@ impl JsonSchema for AssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::Versioned(Box::new(VersionedAssistantSettingsContent::default()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantSettingsContent {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match **settings {
|
||||
match &self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(_) => true,
|
||||
VersionedAssistantSettingsContent::V2(_) => false,
|
||||
},
|
||||
AssistantSettingsContent::Legacy(_) => true,
|
||||
Some(AssistantSettingsContentInner::Legacy(_)) => true,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match **settings {
|
||||
match &self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
@@ -212,7 +221,7 @@ impl AssistantSettingsContent {
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
@@ -237,12 +246,13 @@ impl AssistantSettingsContent {
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
},
|
||||
None => AssistantSettingsContentV2::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match **settings {
|
||||
match &mut self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref mut settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
@@ -250,9 +260,17 @@ impl AssistantSettingsContent {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
None => {
|
||||
self.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
dock: Some(dock),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,8 +278,8 @@ impl AssistantSettingsContent {
|
||||
let model = language_model.id().0.to_string();
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match **settings {
|
||||
match &mut self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref mut settings) => {
|
||||
match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
@@ -337,56 +355,80 @@ impl AssistantSettingsContent {
|
||||
settings.default_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => {
|
||||
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection { provider, model }),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
|
||||
if let AssistantSettingsContent::Versioned(boxed) = self {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
settings.inline_assistant_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
}
|
||||
self.v2_setting(|setting| {
|
||||
setting.inline_assistant_model = Some(LanguageModelSelection { provider, model });
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
|
||||
if let AssistantSettingsContent::Versioned(boxed) = self {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
settings.commit_message_model = Some(LanguageModelSelection { provider, model });
|
||||
self.v2_setting(|setting| {
|
||||
setting.commit_message_model = Some(LanguageModelSelection { provider, model });
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn v2_setting(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut AssistantSettingsContentV2) -> anyhow::Result<()>,
|
||||
) -> anyhow::Result<()> {
|
||||
match self.inner.get_or_insert_with(|| {
|
||||
AssistantSettingsContentInner::for_v2(AssistantSettingsContentV2 {
|
||||
..Default::default()
|
||||
})
|
||||
}) {
|
||||
AssistantSettingsContentInner::Versioned(boxed) => {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
f(settings)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
|
||||
if let AssistantSettingsContent::Versioned(boxed) = self {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
settings.thread_summary_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
}
|
||||
self.v2_setting(|setting| {
|
||||
setting.thread_summary_model = Some(LanguageModelSelection { provider, model });
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
|
||||
let AssistantSettingsContent::Versioned(boxed) = self else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
settings.always_allow_tool_actions = Some(allow);
|
||||
}
|
||||
self.v2_setting(|setting| {
|
||||
setting.always_allow_tool_actions = Some(allow);
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
|
||||
let AssistantSettingsContent::Versioned(boxed) = self else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
settings.default_profile = Some(profile_id);
|
||||
}
|
||||
self.v2_setting(|setting| {
|
||||
setting.default_profile = Some(profile_id);
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn create_profile(
|
||||
@@ -394,11 +436,7 @@ impl AssistantSettingsContent {
|
||||
profile_id: AgentProfileId,
|
||||
profile: AgentProfile,
|
||||
) -> Result<()> {
|
||||
let AssistantSettingsContent::Versioned(boxed) = self else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
self.v2_setting(|settings| {
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
if profiles.contains_key(&profile_id) {
|
||||
bail!("profile with ID '{profile_id}' already exists");
|
||||
@@ -424,9 +462,9 @@ impl AssistantSettingsContent {
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +499,7 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
|
||||
pub struct AssistantSettingsContentV2 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
@@ -708,6 +746,39 @@ impl Settings for AssistantSettings {
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
|
||||
if let Some(b) = vscode
|
||||
.read_value("chat.agent.enabled")
|
||||
.and_then(|b| b.as_bool())
|
||||
{
|
||||
match &mut current.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(versioned)) => {
|
||||
match versioned.as_mut() {
|
||||
VersionedAssistantSettingsContent::V1(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
|
||||
VersionedAssistantSettingsContent::V2(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(AssistantSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
|
||||
None => {
|
||||
current.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
enabled: Some(b),
|
||||
button: Some(b),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
@@ -751,28 +822,30 @@ mod tests {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AssistantSettingsContent::Versioned(Box::new(
|
||||
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
}),
|
||||
))
|
||||
*settings = AssistantSettingsContent {
|
||||
inner: Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
},
|
||||
)),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ impl DocsSlashCommand {
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace was dropped"))?;
|
||||
let project = workspace.read(cx).project().clone();
|
||||
anyhow::Ok(project.read(cx).client().http_client().clone())
|
||||
anyhow::Ok(project.read(cx).client().http_client())
|
||||
});
|
||||
|
||||
if let Some(http_client) = http_client.log_err() {
|
||||
|
||||
@@ -11,6 +11,7 @@ use gpui::{
|
||||
};
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
|
||||
language_settings::SoftWrap,
|
||||
};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -274,7 +275,9 @@ pub struct EditFileToolCard {
|
||||
project: Entity<Project>,
|
||||
diff_task: Option<Task<Result<()>>>,
|
||||
preview_expanded: bool,
|
||||
error_expanded: bool,
|
||||
full_height_expanded: bool,
|
||||
total_lines: Option<u32>,
|
||||
editor_unique_id: EntityId,
|
||||
}
|
||||
|
||||
@@ -293,11 +296,13 @@ impl EditFileToolCard {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.disable_scrolling(cx);
|
||||
editor.disable_expand_excerpt_buttons(cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::None, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
@@ -312,7 +317,9 @@ impl EditFileToolCard {
|
||||
multibuffer,
|
||||
diff_task: None,
|
||||
preview_expanded: true,
|
||||
error_expanded: false,
|
||||
full_height_expanded: false,
|
||||
total_lines: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +336,7 @@ impl EditFileToolCard {
|
||||
let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.multibuffer.update(cx, |multibuffer, cx| {
|
||||
this.total_lines = this.multibuffer.update(cx, |multibuffer, cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let diff = buffer_diff.read(cx);
|
||||
let diff_hunk_ranges = diff
|
||||
@@ -345,7 +352,10 @@ impl EditFileToolCard {
|
||||
);
|
||||
debug_assert!(is_newly_added);
|
||||
multibuffer.add_diff(buffer_diff, cx);
|
||||
let end = multibuffer.len(cx);
|
||||
Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
}));
|
||||
@@ -360,7 +370,10 @@ impl ToolCard for EditFileToolCard {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let failed = matches!(status, ToolUseStatus::Error(_));
|
||||
let (failed, error_message) = match status {
|
||||
ToolUseStatus::Error(err) => (true, Some(err.to_string())),
|
||||
_ => (false, None),
|
||||
};
|
||||
|
||||
let path_label_button = h_flex()
|
||||
.id(("edit-tool-path-label-button", self.editor_unique_id))
|
||||
@@ -452,9 +465,26 @@ impl ToolCard for EditFileToolCard {
|
||||
.map(|container| {
|
||||
if failed {
|
||||
container.child(
|
||||
Icon::new(IconName::Close)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Close)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.child(
|
||||
Disclosure::new(
|
||||
("edit-file-error-disclosure", self.editor_unique_id),
|
||||
self.error_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener(
|
||||
move |this, _event, _window, _cx| {
|
||||
this.error_expanded = !this.error_expanded;
|
||||
},
|
||||
)),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
container.child(
|
||||
@@ -473,8 +503,14 @@ impl ToolCard for EditFileToolCard {
|
||||
}
|
||||
});
|
||||
|
||||
let editor = self.editor.update(cx, |editor, cx| {
|
||||
editor.render(window, cx).into_any_element()
|
||||
let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| {
|
||||
let line_height = editor
|
||||
.style()
|
||||
.map(|style| style.text.line_height_in_pixels(window.rem_size()))
|
||||
.unwrap_or_default();
|
||||
|
||||
let element = editor.render(window, cx);
|
||||
(element.into_any_element(), line_height)
|
||||
});
|
||||
|
||||
let (full_height_icon, full_height_tooltip_label) = if self.full_height_expanded {
|
||||
@@ -498,6 +534,9 @@ impl ToolCard for EditFileToolCard {
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
|
||||
const DEFAULT_COLLAPSED_LINES: u32 = 10;
|
||||
let is_collapsible = self.total_lines.unwrap_or(0) > DEFAULT_COLLAPSED_LINES;
|
||||
|
||||
v_flex()
|
||||
.mb_2()
|
||||
.border_1()
|
||||
@@ -506,50 +545,79 @@ impl ToolCard for EditFileToolCard {
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.child(codeblock_header)
|
||||
.when(!failed && self.preview_expanded, |card| {
|
||||
.when(failed && self.error_expanded, |card| {
|
||||
card.child(
|
||||
v_flex()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.p_2()
|
||||
.gap_1()
|
||||
.border_t_1()
|
||||
.border_dashed()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.map(|editor_container| {
|
||||
if self.full_height_expanded {
|
||||
editor_container.h_full()
|
||||
} else {
|
||||
editor_container.max_h_64()
|
||||
}
|
||||
})
|
||||
.child(div().pl_1().child(editor))
|
||||
.when(!self.full_height_expanded, |editor_container| {
|
||||
editor_container.child(gradient_overlay)
|
||||
}),
|
||||
.rounded_b_md()
|
||||
.child(
|
||||
Label::new("Error")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Error),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.rounded_md()
|
||||
.text_ui_sm(cx)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.children(
|
||||
error_message
|
||||
.map(|error| div().child(error).into_any_element()),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(!failed && self.preview_expanded, |card| {
|
||||
card.child(
|
||||
h_flex()
|
||||
.id(("edit-tool-card-inner-hflex", self.editor_unique_id))
|
||||
.flex_none()
|
||||
.cursor_pointer()
|
||||
.h_5()
|
||||
.justify_center()
|
||||
.rounded_b_md()
|
||||
v_flex()
|
||||
.relative()
|
||||
.map(|editor_container| {
|
||||
if self.full_height_expanded {
|
||||
editor_container.h_full()
|
||||
} else {
|
||||
editor_container
|
||||
.h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
|
||||
}
|
||||
})
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
|
||||
.child(
|
||||
Icon::new(full_height_icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.tooltip(Tooltip::text(full_height_tooltip_label))
|
||||
.on_click(cx.listener(move |this, _event, _window, _cx| {
|
||||
this.full_height_expanded = !this.full_height_expanded;
|
||||
})),
|
||||
.child(div().pl_1().child(editor))
|
||||
.when(
|
||||
!self.full_height_expanded && is_collapsible,
|
||||
|editor_container| editor_container.child(gradient_overlay),
|
||||
),
|
||||
)
|
||||
.when(is_collapsible, |editor_container| {
|
||||
editor_container.child(
|
||||
h_flex()
|
||||
.id(("expand-button", self.editor_unique_id))
|
||||
.flex_none()
|
||||
.cursor_pointer()
|
||||
.h_5()
|
||||
.justify_center()
|
||||
.rounded_b_md()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
|
||||
.child(
|
||||
Icon::new(full_height_icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.tooltip(Tooltip::text(full_height_tooltip_label))
|
||||
.on_click(cx.listener(move |this, _event, _window, _cx| {
|
||||
this.full_height_expanded = !this.full_height_expanded;
|
||||
})),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ The output results will be shown to the user already, only list it again if nece
|
||||
|
||||
Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error.
|
||||
|
||||
Do not use this tool for commands that run indefinitely, such as servers (e.g., `python -m http.server`) or file watchers that don't terminate on their own.
|
||||
Do not use this tool for commands that run indefinitely, such as servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers that don't terminate on their own.
|
||||
|
||||
Remember that each invocation of this tool will spawn a new shell process, so you can't rely on any state from previous invocations.
|
||||
|
||||
@@ -118,6 +118,13 @@ impl Settings for AutoUpdateSetting {
|
||||
|
||||
Ok(Self(auto_update.0))
|
||||
}
|
||||
|
||||
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
|
||||
vscode.enum_setting("update.mode", current, |s| match s {
|
||||
"none" | "manual" => Some(AutoUpdateSettingContent(false)),
|
||||
_ => Some(AutoUpdateSettingContent(true)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -91,7 +91,7 @@ fn view_release_notes_locally(
|
||||
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let tab_content = SharedString::from(body.title.to_string());
|
||||
let editor = cx.new(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), window, cx)
|
||||
});
|
||||
@@ -102,7 +102,7 @@ fn view_release_notes_locally(
|
||||
editor,
|
||||
workspace_handle,
|
||||
language_registry,
|
||||
Some(tab_description),
|
||||
tab_content,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -32,4 +32,6 @@ impl Settings for CallSettings {
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,8 @@ impl Settings for ClientSettings {
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -130,6 +132,10 @@ impl Settings for ProxySettings {
|
||||
.or(sources.default.proxy.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
|
||||
vscode.string_setting("http.proxy", &mut current.proxy);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_settings(cx: &mut App) {
|
||||
@@ -518,6 +524,18 @@ impl settings::Settings for TelemetrySettings {
|
||||
.unwrap_or(sources.default.metrics.ok_or_else(Self::missing_default)?),
|
||||
})
|
||||
}
|
||||
|
||||
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
|
||||
vscode.enum_setting("telemetry.telemetryLevel", &mut current.metrics, |s| {
|
||||
Some(s == "all")
|
||||
});
|
||||
vscode.enum_setting("telemetry.telemetryLevel", &mut current.diagnostics, |s| {
|
||||
Some(matches!(s, "all" | "error" | "crash"))
|
||||
});
|
||||
// we could translate telemetry.telemetryLevel, but just because users didn't want
|
||||
// to send microsoft telemetry doesn't mean they don't want to send it to zed. their
|
||||
// all/error/crash/off correspond to combinations of our "diagnostics" and "metrics".
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -546,7 +564,7 @@ impl Client {
|
||||
|
||||
pub fn production(cx: &mut App) -> Arc<Self> {
|
||||
let clock = Arc::new(clock::RealSystemClock);
|
||||
let http = Arc::new(HttpClientWithUrl::new_uri(
|
||||
let http = Arc::new(HttpClientWithUrl::new_url(
|
||||
cx.http_client(),
|
||||
&ClientSettings::get_global(cx).server_url,
|
||||
cx.http_client().proxy().cloned(),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! socks proxy
|
||||
use anyhow::{Result, anyhow};
|
||||
use http_client::Uri;
|
||||
use http_client::Url;
|
||||
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
|
||||
|
||||
pub(crate) async fn connect_socks_proxy_stream(
|
||||
proxy: Option<&Uri>,
|
||||
proxy: Option<&Url>,
|
||||
rpc_host: (&str, u16),
|
||||
) -> Result<Box<dyn AsyncReadWrite>> {
|
||||
let stream = match parse_socks_proxy(proxy) {
|
||||
@@ -32,9 +32,9 @@ pub(crate) async fn connect_socks_proxy_stream(
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn parse_socks_proxy(proxy: Option<&Uri>) -> Option<((String, u16), SocksVersion)> {
|
||||
let proxy_uri = proxy?;
|
||||
let scheme = proxy_uri.scheme_str()?;
|
||||
fn parse_socks_proxy(proxy: Option<&Url>) -> Option<((String, u16), SocksVersion)> {
|
||||
let proxy_url = proxy?;
|
||||
let scheme = proxy_url.scheme();
|
||||
let socks_version = if scheme.starts_with("socks4") {
|
||||
// socks4
|
||||
SocksVersion::V4
|
||||
@@ -44,7 +44,7 @@ fn parse_socks_proxy(proxy: Option<&Uri>) -> Option<((String, u16), SocksVersion
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
if let (Some(host), Some(port)) = (proxy_uri.host(), proxy_uri.port_u16()) {
|
||||
if let Some((host, port)) = proxy_url.host().zip(proxy_url.port_or_known_default()) {
|
||||
Some(((host.to_string(), port), socks_version))
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
alter table billing_preferences
|
||||
add column model_request_overages_enabled bool not null default false,
|
||||
add column model_request_overages_spend_limit_in_cents integer not null default 0;
|
||||
@@ -0,0 +1,8 @@
|
||||
create table subscription_usage_meters (
|
||||
id serial primary key,
|
||||
subscription_usage_id integer not null references subscription_usages (id) on delete cascade,
|
||||
model_id integer not null references models (id) on delete cascade,
|
||||
requests integer not null default 0
|
||||
);
|
||||
|
||||
create unique index uix_subscription_usage_meters_on_subscription_usage_model on subscription_usage_meters (subscription_usage_id, model_id);
|
||||
@@ -0,0 +1,6 @@
|
||||
alter table subscription_usage_meters
|
||||
add column mode text not null default 'normal';
|
||||
|
||||
drop index uix_subscription_usage_meters_on_subscription_usage_model;
|
||||
|
||||
create unique index uix_subscription_usage_meters_on_subscription_usage_model_mode on subscription_usage_meters (subscription_usage_id, model_id, mode);
|
||||
@@ -152,6 +152,7 @@ struct AuthenticatedUserParams {
|
||||
struct AuthenticatedUserResponse {
|
||||
user: User,
|
||||
metrics_id: String,
|
||||
feature_flags: Vec<String>,
|
||||
}
|
||||
|
||||
async fn get_authenticated_user(
|
||||
@@ -172,7 +173,12 @@ async fn get_authenticated_user(
|
||||
)
|
||||
.await?;
|
||||
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
||||
Ok(Json(AuthenticatedUserResponse { user, metrics_id }))
|
||||
let feature_flags = app.db.get_user_flags(user.id).await?;
|
||||
Ok(Json(AuthenticatedUserResponse {
|
||||
user,
|
||||
metrics_id,
|
||||
feature_flags,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::api::events::SnowflakeRow;
|
||||
use crate::db::billing_subscription::{
|
||||
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
|
||||
};
|
||||
use crate::llm::db::subscription_usage_meter::CompletionMode;
|
||||
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use crate::rpc::{ResultExt as _, Server};
|
||||
use crate::{AppState, Cents, Error, Result};
|
||||
@@ -65,6 +66,8 @@ struct GetBillingPreferencesParams {
|
||||
#[derive(Debug, Serialize)]
|
||||
struct BillingPreferencesResponse {
|
||||
max_monthly_llm_usage_spending_in_cents: i32,
|
||||
model_request_overages_enabled: bool,
|
||||
model_request_overages_spend_limit_in_cents: i32,
|
||||
}
|
||||
|
||||
async fn get_billing_preferences(
|
||||
@@ -81,16 +84,30 @@ async fn get_billing_preferences(
|
||||
|
||||
Ok(Json(BillingPreferencesResponse {
|
||||
max_monthly_llm_usage_spending_in_cents: preferences
|
||||
.as_ref()
|
||||
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0 as i32, |preferences| {
|
||||
preferences.max_monthly_llm_usage_spending_in_cents
|
||||
}),
|
||||
model_request_overages_enabled: preferences.as_ref().map_or(false, |preferences| {
|
||||
preferences.model_request_overages_enabled
|
||||
}),
|
||||
model_request_overages_spend_limit_in_cents: preferences
|
||||
.as_ref()
|
||||
.map_or(0, |preferences| {
|
||||
preferences.model_request_overages_spend_limit_in_cents
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UpdateBillingPreferencesBody {
|
||||
github_user_id: i32,
|
||||
#[serde(default)]
|
||||
max_monthly_llm_usage_spending_in_cents: i32,
|
||||
#[serde(default)]
|
||||
model_request_overages_enabled: bool,
|
||||
#[serde(default)]
|
||||
model_request_overages_spend_limit_in_cents: i32,
|
||||
}
|
||||
|
||||
async fn update_billing_preferences(
|
||||
@@ -106,6 +123,8 @@ async fn update_billing_preferences(
|
||||
|
||||
let max_monthly_llm_usage_spending_in_cents =
|
||||
body.max_monthly_llm_usage_spending_in_cents.max(0);
|
||||
let model_request_overages_spend_limit_in_cents =
|
||||
body.model_request_overages_spend_limit_in_cents.max(0);
|
||||
|
||||
let billing_preferences =
|
||||
if let Some(_billing_preferences) = app.db.get_billing_preferences(user.id).await? {
|
||||
@@ -116,6 +135,12 @@ async fn update_billing_preferences(
|
||||
max_monthly_llm_usage_spending_in_cents: ActiveValue::set(
|
||||
max_monthly_llm_usage_spending_in_cents,
|
||||
),
|
||||
model_request_overages_enabled: ActiveValue::set(
|
||||
body.model_request_overages_enabled,
|
||||
),
|
||||
model_request_overages_spend_limit_in_cents: ActiveValue::set(
|
||||
model_request_overages_spend_limit_in_cents,
|
||||
),
|
||||
},
|
||||
)
|
||||
.await?
|
||||
@@ -125,18 +150,22 @@ async fn update_billing_preferences(
|
||||
user.id,
|
||||
&crate::db::CreateBillingPreferencesParams {
|
||||
max_monthly_llm_usage_spending_in_cents,
|
||||
model_request_overages_enabled: body.model_request_overages_enabled,
|
||||
model_request_overages_spend_limit_in_cents,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
SnowflakeRow::new(
|
||||
"Spend Limit Updated",
|
||||
"Billing Preferences Updated",
|
||||
Some(user.metrics_id),
|
||||
user.admin,
|
||||
None,
|
||||
json!({
|
||||
"user_id": user.id,
|
||||
"model_request_overages_enabled": billing_preferences.model_request_overages_enabled,
|
||||
"model_request_overages_spend_limit_in_cents": billing_preferences.model_request_overages_spend_limit_in_cents,
|
||||
"max_monthly_llm_usage_spending_in_cents": billing_preferences.max_monthly_llm_usage_spending_in_cents,
|
||||
}),
|
||||
)
|
||||
@@ -149,6 +178,9 @@ async fn update_billing_preferences(
|
||||
Ok(Json(BillingPreferencesResponse {
|
||||
max_monthly_llm_usage_spending_in_cents: billing_preferences
|
||||
.max_monthly_llm_usage_spending_in_cents,
|
||||
model_request_overages_enabled: billing_preferences.model_request_overages_enabled,
|
||||
model_request_overages_spend_limit_in_cents: billing_preferences
|
||||
.model_request_overages_spend_limit_in_cents,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -291,16 +323,35 @@ async fn create_billing_subscription(
|
||||
CustomerId::from_str(&existing_customer.stripe_customer_id)
|
||||
.context("failed to parse customer ID")?
|
||||
} else {
|
||||
let customer = Customer::create(
|
||||
&stripe_client,
|
||||
CreateCustomer {
|
||||
email: user.email_address.as_deref(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let existing_customer = if let Some(email) = user.email_address.as_deref() {
|
||||
let customers = Customer::list(
|
||||
&stripe_client,
|
||||
&stripe::ListCustomers {
|
||||
email: Some(email),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
customer.id
|
||||
customers.data.first().cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(existing_customer) = existing_customer {
|
||||
existing_customer.id
|
||||
} else {
|
||||
let customer = Customer::create(
|
||||
&stripe_client,
|
||||
CreateCustomer {
|
||||
email: user.email_address.as_deref(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
customer.id
|
||||
}
|
||||
};
|
||||
|
||||
let success_url = format!(
|
||||
@@ -329,11 +380,14 @@ async fn create_billing_subscription(
|
||||
}
|
||||
}
|
||||
|
||||
let feature_flags = app.db.get_user_flags(user.id).await?;
|
||||
|
||||
stripe_billing
|
||||
.checkout_with_zed_pro_trial(
|
||||
app.config.zed_pro_price_id()?,
|
||||
customer_id,
|
||||
&user.github_login,
|
||||
feature_flags,
|
||||
&success_url,
|
||||
)
|
||||
.await?
|
||||
@@ -343,7 +397,9 @@ async fn create_billing_subscription(
|
||||
zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
"claude-3-7-sonnet",
|
||||
)?;
|
||||
let stripe_model = stripe_billing.register_model(default_model).await?;
|
||||
let stripe_model = stripe_billing
|
||||
.register_model_for_token_based_usage(default_model)
|
||||
.await?;
|
||||
stripe_billing
|
||||
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
|
||||
.await?
|
||||
@@ -1193,9 +1249,9 @@ async fn find_or_create_billing_customer(
|
||||
Ok(Some(billing_customer))
|
||||
}
|
||||
|
||||
const SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
const SYNC_LLM_TOKEN_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
pub fn sync_llm_usage_with_stripe_periodically(app: Arc<AppState>) {
|
||||
pub fn sync_llm_token_usage_with_stripe_periodically(app: Arc<AppState>) {
|
||||
let Some(stripe_billing) = app.stripe_billing.clone() else {
|
||||
log::warn!("failed to retrieve Stripe billing object");
|
||||
return;
|
||||
@@ -1210,17 +1266,19 @@ pub fn sync_llm_usage_with_stripe_periodically(app: Arc<AppState>) {
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
loop {
|
||||
sync_with_stripe(&app, &llm_db, &stripe_billing)
|
||||
sync_token_usage_with_stripe(&app, &llm_db, &stripe_billing)
|
||||
.await
|
||||
.context("failed to sync LLM usage to Stripe")
|
||||
.trace_err();
|
||||
executor.sleep(SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL).await;
|
||||
executor
|
||||
.sleep(SYNC_LLM_TOKEN_USAGE_WITH_STRIPE_INTERVAL)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn sync_with_stripe(
|
||||
async fn sync_token_usage_with_stripe(
|
||||
app: &Arc<AppState>,
|
||||
llm_db: &Arc<LlmDatabase>,
|
||||
stripe_billing: &Arc<StripeBilling>,
|
||||
@@ -1251,15 +1309,128 @@ async fn sync_with_stripe(
|
||||
.parse()
|
||||
.context("failed to parse stripe customer id from db")?;
|
||||
|
||||
let stripe_model = stripe_billing.register_model(&model).await?;
|
||||
let stripe_model = stripe_billing
|
||||
.register_model_for_token_based_usage(&model)
|
||||
.await?;
|
||||
stripe_billing
|
||||
.subscribe_to_model(&stripe_subscription_id, &stripe_model)
|
||||
.await?;
|
||||
stripe_billing
|
||||
.bill_model_usage(&stripe_customer_id, &stripe_model, &event)
|
||||
.bill_model_token_usage(&stripe_customer_id, &stripe_model, &event)
|
||||
.await?;
|
||||
llm_db.consume_billing_event(event.id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const SYNC_LLM_REQUEST_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
pub fn sync_llm_request_usage_with_stripe_periodically(app: Arc<AppState>) {
|
||||
let Some(stripe_billing) = app.stripe_billing.clone() else {
|
||||
log::warn!("failed to retrieve Stripe billing object");
|
||||
return;
|
||||
};
|
||||
let Some(llm_db) = app.llm_db.clone() else {
|
||||
log::warn!("failed to retrieve LLM database");
|
||||
return;
|
||||
};
|
||||
|
||||
let executor = app.executor.clone();
|
||||
executor.spawn_detached({
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
loop {
|
||||
sync_model_request_usage_with_stripe(&app, &llm_db, &stripe_billing)
|
||||
.await
|
||||
.context("failed to sync LLM request usage to Stripe")
|
||||
.trace_err();
|
||||
executor
|
||||
.sleep(SYNC_LLM_REQUEST_USAGE_WITH_STRIPE_INTERVAL)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn sync_model_request_usage_with_stripe(
|
||||
app: &Arc<AppState>,
|
||||
llm_db: &Arc<LlmDatabase>,
|
||||
stripe_billing: &Arc<StripeBilling>,
|
||||
) -> anyhow::Result<()> {
|
||||
let usage_meters = llm_db
|
||||
.get_current_subscription_usage_meters(Utc::now())
|
||||
.await?;
|
||||
let user_ids = usage_meters
|
||||
.iter()
|
||||
.map(|(_, usage)| usage.user_id)
|
||||
.collect::<HashSet<UserId>>();
|
||||
let billing_subscriptions = app
|
||||
.db
|
||||
.get_active_zed_pro_billing_subscriptions(user_ids)
|
||||
.await?;
|
||||
|
||||
let claude_3_5_sonnet = stripe_billing
|
||||
.find_price_by_lookup_key("claude-3-5-sonnet-requests")
|
||||
.await?;
|
||||
let claude_3_7_sonnet = stripe_billing
|
||||
.find_price_by_lookup_key("claude-3-7-sonnet-requests")
|
||||
.await?;
|
||||
let claude_3_7_sonnet_max = stripe_billing
|
||||
.find_price_by_lookup_key("claude-3-7-sonnet-requests-max")
|
||||
.await?;
|
||||
|
||||
for (usage_meter, usage) in usage_meters {
|
||||
maybe!(async {
|
||||
let Some((billing_customer, billing_subscription)) =
|
||||
billing_subscriptions.get(&usage.user_id)
|
||||
else {
|
||||
bail!(
|
||||
"Attempted to sync usage meter for user who is not a Stripe customer: {}",
|
||||
usage.user_id
|
||||
);
|
||||
};
|
||||
|
||||
let stripe_customer_id = billing_customer
|
||||
.stripe_customer_id
|
||||
.parse::<stripe::CustomerId>()
|
||||
.context("failed to parse Stripe customer ID from database")?;
|
||||
let stripe_subscription_id = billing_subscription
|
||||
.stripe_subscription_id
|
||||
.parse::<stripe::SubscriptionId>()
|
||||
.context("failed to parse Stripe subscription ID from database")?;
|
||||
|
||||
let model = llm_db.model_by_id(usage_meter.model_id)?;
|
||||
|
||||
let (price_id, meter_event_name) = match model.name.as_str() {
|
||||
"claude-3-5-sonnet" => (&claude_3_5_sonnet.id, "claude_3_5_sonnet/requests"),
|
||||
"claude-3-7-sonnet" => match usage_meter.mode {
|
||||
CompletionMode::Normal => (&claude_3_7_sonnet.id, "claude_3_7_sonnet/requests"),
|
||||
CompletionMode::Max => {
|
||||
(&claude_3_7_sonnet_max.id, "claude_3_7_sonnet/requests/max")
|
||||
}
|
||||
},
|
||||
model_name => {
|
||||
bail!("Attempted to sync usage meter for unsupported model: {model_name:?}")
|
||||
}
|
||||
};
|
||||
|
||||
stripe_billing
|
||||
.subscribe_to_price(&stripe_subscription_id, price_id)
|
||||
.await?;
|
||||
stripe_billing
|
||||
.bill_model_request_usage(
|
||||
&stripe_customer_id,
|
||||
meter_event_name,
|
||||
usage_meter.requests,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -800,6 +800,7 @@ impl LocalSettingsKind {
|
||||
proto::LocalSettingsKind::Settings => Self::Settings,
|
||||
proto::LocalSettingsKind::Tasks => Self::Tasks,
|
||||
proto::LocalSettingsKind::Editorconfig => Self::Editorconfig,
|
||||
proto::LocalSettingsKind::Debug => Self::Debug,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,6 +809,7 @@ impl LocalSettingsKind {
|
||||
Self::Settings => proto::LocalSettingsKind::Settings,
|
||||
Self::Tasks => proto::LocalSettingsKind::Tasks,
|
||||
Self::Editorconfig => proto::LocalSettingsKind::Editorconfig,
|
||||
Self::Debug => proto::LocalSettingsKind::Debug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ pub mod messages;
|
||||
pub mod notifications;
|
||||
pub mod processed_stripe_events;
|
||||
pub mod projects;
|
||||
pub mod rate_buckets;
|
||||
pub mod rooms;
|
||||
pub mod servers;
|
||||
pub mod users;
|
||||
|
||||
@@ -3,11 +3,15 @@ use super::*;
|
||||
#[derive(Debug)]
|
||||
pub struct CreateBillingPreferencesParams {
|
||||
pub max_monthly_llm_usage_spending_in_cents: i32,
|
||||
pub model_request_overages_enabled: bool,
|
||||
pub model_request_overages_spend_limit_in_cents: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct UpdateBillingPreferencesParams {
|
||||
pub max_monthly_llm_usage_spending_in_cents: ActiveValue<i32>,
|
||||
pub model_request_overages_enabled: ActiveValue<bool>,
|
||||
pub model_request_overages_spend_limit_in_cents: ActiveValue<i32>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
@@ -37,6 +41,12 @@ impl Database {
|
||||
max_monthly_llm_usage_spending_in_cents: ActiveValue::set(
|
||||
params.max_monthly_llm_usage_spending_in_cents,
|
||||
),
|
||||
model_request_overages_enabled: ActiveValue::set(
|
||||
params.model_request_overages_enabled,
|
||||
),
|
||||
model_request_overages_spend_limit_in_cents: ActiveValue::set(
|
||||
params.model_request_overages_spend_limit_in_cents,
|
||||
),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_with_returning(&*tx)
|
||||
@@ -59,6 +69,10 @@ impl Database {
|
||||
max_monthly_llm_usage_spending_in_cents: params
|
||||
.max_monthly_llm_usage_spending_in_cents
|
||||
.clone(),
|
||||
model_request_overages_enabled: params.model_request_overages_enabled.clone(),
|
||||
model_request_overages_spend_limit_in_cents: params
|
||||
.model_request_overages_spend_limit_in_cents
|
||||
.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
.filter(billing_preference::Column::UserId.eq(user_id))
|
||||
|
||||
@@ -191,6 +191,38 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_active_zed_pro_billing_subscriptions(
|
||||
&self,
|
||||
user_ids: HashSet<UserId>,
|
||||
) -> Result<HashMap<UserId, (billing_customer::Model, billing_subscription::Model)>> {
|
||||
self.transaction(|tx| {
|
||||
let user_ids = user_ids.clone();
|
||||
async move {
|
||||
let mut rows = billing_subscription::Entity::find()
|
||||
.inner_join(billing_customer::Entity)
|
||||
.select_also(billing_customer::Entity)
|
||||
.filter(billing_customer::Column::UserId.is_in(user_ids))
|
||||
.filter(
|
||||
billing_subscription::Column::StripeSubscriptionStatus
|
||||
.eq(StripeSubscriptionStatus::Active),
|
||||
)
|
||||
.filter(billing_subscription::Column::Kind.eq(SubscriptionKind::ZedPro))
|
||||
.order_by_asc(billing_subscription::Column::Id)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut subscriptions = HashMap::default();
|
||||
while let Some(row) = rows.next().await {
|
||||
if let (subscription, Some(customer)) = row? {
|
||||
subscriptions.insert(customer.user_id, (customer, subscription));
|
||||
}
|
||||
}
|
||||
Ok(subscriptions)
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns whether the user has an active billing subscription.
|
||||
pub async fn has_active_billing_subscription(&self, user_id: UserId) -> Result<bool> {
|
||||
Ok(self.count_active_billing_subscriptions(user_id).await? > 0)
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
use super::*;
|
||||
use crate::db::tables::rate_buckets;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
|
||||
impl Database {
|
||||
/// Saves the rate limit for the given user and rate limit name if the last_refill is later
|
||||
/// than the currently saved timestamp.
|
||||
pub async fn save_rate_buckets(&self, buckets: &[rate_buckets::Model]) -> Result<()> {
|
||||
if buckets.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.transaction(|tx| async move {
|
||||
rate_buckets::Entity::insert_many(buckets.iter().map(|bucket| {
|
||||
rate_buckets::ActiveModel {
|
||||
user_id: ActiveValue::Set(bucket.user_id),
|
||||
rate_limit_name: ActiveValue::Set(bucket.rate_limit_name.clone()),
|
||||
token_count: ActiveValue::Set(bucket.token_count),
|
||||
last_refill: ActiveValue::Set(bucket.last_refill),
|
||||
}
|
||||
}))
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
rate_buckets::Column::UserId,
|
||||
rate_buckets::Column::RateLimitName,
|
||||
])
|
||||
.update_columns([
|
||||
rate_buckets::Column::TokenCount,
|
||||
rate_buckets::Column::LastRefill,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Retrieves the rate limit for the given user and rate limit name.
|
||||
pub async fn get_rate_bucket(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
rate_limit_name: &str,
|
||||
) -> Result<Option<rate_buckets::Model>> {
|
||||
self.transaction(|tx| async move {
|
||||
let rate_limit = rate_buckets::Entity::find()
|
||||
.filter(rate_buckets::Column::UserId.eq(user_id))
|
||||
.filter(rate_buckets::Column::RateLimitName.eq(rate_limit_name))
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(rate_limit)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ pub mod project;
|
||||
pub mod project_collaborator;
|
||||
pub mod project_repository;
|
||||
pub mod project_repository_statuses;
|
||||
pub mod rate_buckets;
|
||||
pub mod room;
|
||||
pub mod room_participant;
|
||||
pub mod server;
|
||||
|
||||
@@ -9,6 +9,8 @@ pub struct Model {
|
||||
pub created_at: DateTime,
|
||||
pub user_id: UserId,
|
||||
pub max_monthly_llm_usage_spending_in_cents: i32,
|
||||
pub model_request_overages_enabled: bool,
|
||||
pub model_request_overages_spend_limit_in_cents: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
use crate::db::UserId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "rate_buckets")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub user_id: UserId,
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub rate_limit_name: String,
|
||||
pub token_count: i32,
|
||||
pub last_refill: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -32,4 +32,6 @@ pub enum LocalSettingsKind {
|
||||
Tasks,
|
||||
#[sea_orm(string_value = "editorconfig")]
|
||||
Editorconfig,
|
||||
#[sea_orm(string_value = "debug")]
|
||||
Debug,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ pub mod env;
|
||||
pub mod executor;
|
||||
pub mod llm;
|
||||
pub mod migrations;
|
||||
mod rate_limiter;
|
||||
pub mod rpc;
|
||||
pub mod seed;
|
||||
pub mod stripe_billing;
|
||||
@@ -25,7 +24,6 @@ pub use cents::*;
|
||||
use db::{ChannelId, Database};
|
||||
use executor::Executor;
|
||||
use llm::db::LlmDatabase;
|
||||
pub use rate_limiter::*;
|
||||
use serde::Deserialize;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use util::ResultExt;
|
||||
@@ -295,7 +293,6 @@ pub struct AppState {
|
||||
pub blob_store_client: Option<aws_sdk_s3::Client>,
|
||||
pub stripe_client: Option<Arc<stripe::Client>>,
|
||||
pub stripe_billing: Option<Arc<StripeBilling>>,
|
||||
pub rate_limiter: Arc<RateLimiter>,
|
||||
pub executor: Executor,
|
||||
pub kinesis_client: Option<::aws_sdk_kinesis::Client>,
|
||||
pub config: Config,
|
||||
@@ -348,7 +345,6 @@ impl AppState {
|
||||
.clone()
|
||||
.map(|stripe_client| Arc::new(StripeBilling::new(stripe_client))),
|
||||
stripe_client,
|
||||
rate_limiter: Arc::new(RateLimiter::new(db)),
|
||||
executor,
|
||||
kinesis_client: if config.kinesis_access_key.is_some() {
|
||||
build_kinesis_client(&config).await.log_err()
|
||||
|
||||
@@ -5,6 +5,8 @@ use crate::Cents;
|
||||
|
||||
pub use token::*;
|
||||
|
||||
pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
|
||||
|
||||
/// The maximum monthly spending an individual user can reach on the free tier
|
||||
/// before they have to pay.
|
||||
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10);
|
||||
|
||||
@@ -2,5 +2,6 @@ use super::*;
|
||||
|
||||
pub mod billing_events;
|
||||
pub mod providers;
|
||||
pub mod subscription_usage_meters;
|
||||
pub mod subscription_usages;
|
||||
pub mod usages;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
use crate::llm::db::queries::subscription_usages::convert_chrono_to_time;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl LlmDatabase {
|
||||
/// Returns all current subscription usage meters as of the given timestamp.
|
||||
pub async fn get_current_subscription_usage_meters(
|
||||
&self,
|
||||
now: DateTimeUtc,
|
||||
) -> Result<Vec<(subscription_usage_meter::Model, subscription_usage::Model)>> {
|
||||
let now = convert_chrono_to_time(now)?;
|
||||
|
||||
self.transaction(|tx| async move {
|
||||
let result = subscription_usage_meter::Entity::find()
|
||||
.inner_join(subscription_usage::Entity)
|
||||
.filter(
|
||||
subscription_usage::Column::PeriodStartAt
|
||||
.lte(now)
|
||||
.and(subscription_usage::Column::PeriodEndAt.gte(now)),
|
||||
)
|
||||
.select_also(subscription_usage::Entity)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let result = result
|
||||
.into_iter()
|
||||
.filter_map(|(meter, usage)| {
|
||||
let usage = usage?;
|
||||
Some((meter, usage))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use crate::db::{UserId, billing_subscription};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn convert_chrono_to_time(datetime: DateTimeUtc) -> anyhow::Result<PrimitiveDateTime> {
|
||||
pub fn convert_chrono_to_time(datetime: DateTimeUtc) -> anyhow::Result<PrimitiveDateTime> {
|
||||
use chrono::{Datelike as _, Timelike as _};
|
||||
|
||||
let date = time::Date::from_calendar_date(
|
||||
|
||||
@@ -3,5 +3,6 @@ pub mod model;
|
||||
pub mod monthly_usage;
|
||||
pub mod provider;
|
||||
pub mod subscription_usage;
|
||||
pub mod subscription_usage_meter;
|
||||
pub mod usage;
|
||||
pub mod usage_measure;
|
||||
|
||||
55
crates/collab/src/llm/db/tables/subscription_usage_meter.rs
Normal file
55
crates/collab/src/llm/db/tables/subscription_usage_meter.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::llm::db::ModelId;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "subscription_usage_meters")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub subscription_usage_id: i32,
|
||||
pub model_id: ModelId,
|
||||
pub mode: CompletionMode,
|
||||
pub requests: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::subscription_usage::Entity",
|
||||
from = "Column::SubscriptionUsageId",
|
||||
to = "super::subscription_usage::Column::Id"
|
||||
)]
|
||||
SubscriptionUsage,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::model::Entity",
|
||||
from = "Column::ModelId",
|
||||
to = "super::model::Column::Id"
|
||||
)]
|
||||
Model,
|
||||
}
|
||||
|
||||
impl Related<super::subscription_usage::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::SubscriptionUsage.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::model::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Model.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionMode {
|
||||
#[sea_orm(string_value = "normal")]
|
||||
Normal,
|
||||
#[sea_orm(string_value = "max")]
|
||||
Max,
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::Cents;
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::{billing_subscription, user};
|
||||
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use crate::llm::{
|
||||
AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
};
|
||||
use crate::{Config, db::billing_preference};
|
||||
use anyhow::{Result, anyhow};
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
@@ -32,8 +34,14 @@ pub struct LlmTokenClaims {
|
||||
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
|
||||
pub plan: Plan,
|
||||
#[serde(default)]
|
||||
pub has_extended_trial: bool,
|
||||
#[serde(default)]
|
||||
pub subscription_period: Option<(NaiveDateTime, NaiveDateTime)>,
|
||||
#[serde(default)]
|
||||
pub enable_model_request_overages: bool,
|
||||
#[serde(default)]
|
||||
pub model_request_overages_spend_limit_in_cents: u32,
|
||||
#[serde(default)]
|
||||
pub can_use_web_search_tool: bool,
|
||||
}
|
||||
|
||||
@@ -75,6 +83,7 @@ impl LlmTokenClaims {
|
||||
can_use_web_search_tool: feature_flags.iter().any(|flag| flag == "assistant2"),
|
||||
has_llm_subscription: has_legacy_llm_subscription,
|
||||
max_monthly_spend_in_cents: billing_preferences
|
||||
.as_ref()
|
||||
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {
|
||||
preferences.max_monthly_llm_usage_spending_in_cents as u32
|
||||
}),
|
||||
@@ -89,6 +98,9 @@ impl LlmTokenClaims {
|
||||
SubscriptionKind::ZedPro => Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
|
||||
}),
|
||||
has_extended_trial: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG),
|
||||
subscription_period: maybe!({
|
||||
let subscription = subscription?;
|
||||
let period_start_at = subscription.current_period_start_at()?;
|
||||
@@ -96,6 +108,16 @@ impl LlmTokenClaims {
|
||||
|
||||
Some((period_start_at.naive_utc(), period_end_at.naive_utc()))
|
||||
}),
|
||||
enable_model_request_overages: billing_preferences
|
||||
.as_ref()
|
||||
.map_or(false, |preferences| {
|
||||
preferences.model_request_overages_enabled
|
||||
}),
|
||||
model_request_overages_spend_limit_in_cents: billing_preferences
|
||||
.as_ref()
|
||||
.map_or(0, |preferences| {
|
||||
preferences.model_request_overages_spend_limit_in_cents as u32
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(jsonwebtoken::encode(
|
||||
|
||||
@@ -8,13 +8,15 @@ use axum::{
|
||||
};
|
||||
|
||||
use collab::api::CloudflareIpCountryHeader;
|
||||
use collab::api::billing::sync_llm_usage_with_stripe_periodically;
|
||||
use collab::api::billing::{
|
||||
sync_llm_request_usage_with_stripe_periodically, sync_llm_token_usage_with_stripe_periodically,
|
||||
};
|
||||
use collab::llm::db::LlmDatabase;
|
||||
use collab::migrations::run_database_migrations;
|
||||
use collab::user_backfiller::spawn_user_backfiller;
|
||||
use collab::{
|
||||
AppState, Config, RateLimiter, Result, api::fetch_extensions_from_blob_store_periodically, db,
|
||||
env, executor::Executor, rpc::ResultExt,
|
||||
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;
|
||||
@@ -111,10 +113,6 @@ async fn main() -> Result<()> {
|
||||
|
||||
if mode.is_collab() {
|
||||
state.db.purge_old_embeddings().await.trace_err();
|
||||
RateLimiter::save_periodically(
|
||||
state.rate_limiter.clone(),
|
||||
state.executor.clone(),
|
||||
);
|
||||
|
||||
let epoch = state
|
||||
.db
|
||||
@@ -156,7 +154,8 @@ async fn main() -> Result<()> {
|
||||
|
||||
if let Some(mut llm_db) = llm_db {
|
||||
llm_db.initialize().await?;
|
||||
sync_llm_usage_with_stripe_periodically(state.clone());
|
||||
sync_llm_request_usage_with_stripe_periodically(state.clone());
|
||||
sync_llm_token_usage_with_stripe_periodically(state.clone());
|
||||
}
|
||||
|
||||
app = app
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
use crate::{Database, Error, Result, db::UserId, executor::Executor};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use rpc::ErrorCodeExt;
|
||||
use sea_orm::prelude::DateTimeUtc;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
pub trait RateLimit: Send + Sync {
|
||||
fn capacity(&self) -> usize;
|
||||
fn refill_duration(&self) -> Duration;
|
||||
fn db_name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
/// Used to enforce per-user rate limits
|
||||
pub struct RateLimiter {
|
||||
buckets: DashMap<(UserId, String), RateBucket>,
|
||||
dirty_buckets: DashSet<(UserId, String)>,
|
||||
db: Arc<Database>,
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
pub fn new(db: Arc<Database>) -> Self {
|
||||
RateLimiter {
|
||||
buckets: DashMap::new(),
|
||||
dirty_buckets: DashSet::new(),
|
||||
db,
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a new task that periodically saves rate limit data to the database.
|
||||
pub fn save_periodically(rate_limiter: Arc<Self>, executor: Executor) {
|
||||
const RATE_LIMITER_SAVE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
executor.clone().spawn_detached(async move {
|
||||
loop {
|
||||
executor.sleep(RATE_LIMITER_SAVE_INTERVAL).await;
|
||||
rate_limiter.save().await.log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns an error if the user has exceeded the specified `RateLimit`.
|
||||
/// Attempts to read the from the database if no cached RateBucket currently exists.
|
||||
pub async fn check(&self, limit: &dyn RateLimit, user_id: UserId) -> Result<()> {
|
||||
self.check_internal(limit, user_id, Utc::now()).await
|
||||
}
|
||||
|
||||
async fn check_internal(
|
||||
&self,
|
||||
limit: &dyn RateLimit,
|
||||
user_id: UserId,
|
||||
now: DateTimeUtc,
|
||||
) -> Result<()> {
|
||||
let bucket_key = (user_id, limit.db_name().to_string());
|
||||
|
||||
// Attempt to fetch the bucket from the database if it hasn't been cached.
|
||||
// For now, we keep buckets in memory for the lifetime of the process rather than expiring them,
|
||||
// but this enforces limits across restarts so long as the database is reachable.
|
||||
if !self.buckets.contains_key(&bucket_key) {
|
||||
if let Some(bucket) = self.load_bucket(limit, user_id).await.log_err().flatten() {
|
||||
self.buckets.insert(bucket_key.clone(), bucket);
|
||||
self.dirty_buckets.insert(bucket_key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut bucket = self
|
||||
.buckets
|
||||
.entry(bucket_key.clone())
|
||||
.or_insert_with(|| RateBucket::new(limit, now));
|
||||
|
||||
if bucket.value_mut().allow(now) {
|
||||
self.dirty_buckets.insert(bucket_key);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(rpc::proto::ErrorCode::RateLimitExceeded
|
||||
.message("rate limit exceeded".into())
|
||||
.anyhow())?
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_bucket(
|
||||
&self,
|
||||
limit: &dyn RateLimit,
|
||||
user_id: UserId,
|
||||
) -> Result<Option<RateBucket>, Error> {
|
||||
Ok(self
|
||||
.db
|
||||
.get_rate_bucket(user_id, limit.db_name())
|
||||
.await?
|
||||
.map(|saved_bucket| {
|
||||
RateBucket::from_db(
|
||||
limit,
|
||||
saved_bucket.token_count as usize,
|
||||
DateTime::from_naive_utc_and_offset(saved_bucket.last_refill, Utc),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn save(&self) -> Result<()> {
|
||||
let mut buckets = Vec::new();
|
||||
self.dirty_buckets.retain(|key| {
|
||||
if let Some(bucket) = self.buckets.get(key) {
|
||||
buckets.push(crate::db::rate_buckets::Model {
|
||||
user_id: key.0,
|
||||
rate_limit_name: key.1.clone(),
|
||||
token_count: bucket.token_count as i32,
|
||||
last_refill: bucket.last_refill.naive_utc(),
|
||||
});
|
||||
}
|
||||
false
|
||||
});
|
||||
|
||||
match self.db.save_rate_buckets(&buckets).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) => {
|
||||
for bucket in buckets {
|
||||
self.dirty_buckets
|
||||
.insert((bucket.user_id, bucket.rate_limit_name));
|
||||
}
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RateBucket {
|
||||
capacity: usize,
|
||||
token_count: usize,
|
||||
refill_time_per_token: Duration,
|
||||
last_refill: DateTimeUtc,
|
||||
}
|
||||
|
||||
impl RateBucket {
|
||||
fn new(limit: &dyn RateLimit, now: DateTimeUtc) -> Self {
|
||||
Self {
|
||||
capacity: limit.capacity(),
|
||||
token_count: limit.capacity(),
|
||||
refill_time_per_token: limit.refill_duration() / limit.capacity() as i32,
|
||||
last_refill: now,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_db(limit: &dyn RateLimit, token_count: usize, last_refill: DateTimeUtc) -> Self {
|
||||
Self {
|
||||
capacity: limit.capacity(),
|
||||
token_count,
|
||||
refill_time_per_token: limit.refill_duration() / limit.capacity() as i32,
|
||||
last_refill,
|
||||
}
|
||||
}
|
||||
|
||||
fn allow(&mut self, now: DateTimeUtc) -> bool {
|
||||
self.refill(now);
|
||||
if self.token_count > 0 {
|
||||
self.token_count -= 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn refill(&mut self, now: DateTimeUtc) {
|
||||
let elapsed = now - self.last_refill;
|
||||
if elapsed >= self.refill_time_per_token {
|
||||
let new_tokens =
|
||||
elapsed.num_milliseconds() / self.refill_time_per_token.num_milliseconds();
|
||||
self.token_count = (self.token_count + new_tokens as usize).min(self.capacity);
|
||||
|
||||
let unused_refill_time = Duration::milliseconds(
|
||||
elapsed.num_milliseconds() % self.refill_time_per_token.num_milliseconds(),
|
||||
);
|
||||
self.last_refill = now - unused_refill_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::db::{NewUserParams, TestDb};
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rate_limiter(cx: &mut TestAppContext) {
|
||||
let test_db = TestDb::sqlite(cx.executor().clone());
|
||||
let db = test_db.db().clone();
|
||||
let user_1 = db
|
||||
.create_user(
|
||||
"user-1@zed.dev",
|
||||
None,
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user-1".into(),
|
||||
github_user_id: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let user_2 = db
|
||||
.create_user(
|
||||
"user-2@zed.dev",
|
||||
None,
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user-2".into(),
|
||||
github_user_id: 2,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let mut now = Utc::now();
|
||||
|
||||
let rate_limiter = RateLimiter::new(db.clone());
|
||||
let rate_limit_a = Box::new(RateLimitA);
|
||||
let rate_limit_b = Box::new(RateLimitB);
|
||||
|
||||
// User 1 can access resource A two times before being rate-limited.
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_a, user_1, now)
|
||||
.await
|
||||
.unwrap();
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_a, user_1, now)
|
||||
.await
|
||||
.unwrap();
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_a, user_1, now)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// User 2 can access resource A and user 1 can access resource B.
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_b, user_2, now)
|
||||
.await
|
||||
.unwrap();
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_b, user_1, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// After 1.5s, user 1 can make another request before being rate-limited again.
|
||||
now += Duration::milliseconds(1500);
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_a, user_1, now)
|
||||
.await
|
||||
.unwrap();
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_a, user_1, now)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// After 500ms, user 1 can make another request before being rate-limited again.
|
||||
now += Duration::milliseconds(500);
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_a, user_1, now)
|
||||
.await
|
||||
.unwrap();
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_a, user_1, now)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
rate_limiter.save().await.unwrap();
|
||||
|
||||
// Rate limits are reloaded from the database, so user A is still rate-limited
|
||||
// for resource A.
|
||||
let rate_limiter = RateLimiter::new(db.clone());
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_a, user_1, now)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// After 1s, user 1 can make another request before being rate-limited again.
|
||||
now += Duration::seconds(1);
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_a, user_1, now)
|
||||
.await
|
||||
.unwrap();
|
||||
rate_limiter
|
||||
.check_internal(&*rate_limit_a, user_1, now)
|
||||
.await
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
struct RateLimitA;
|
||||
|
||||
impl RateLimit for RateLimitA {
|
||||
fn capacity(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
fn refill_duration(&self) -> Duration {
|
||||
Duration::seconds(2)
|
||||
}
|
||||
|
||||
fn db_name(&self) -> &'static str {
|
||||
"rate-limit-a"
|
||||
}
|
||||
}
|
||||
|
||||
struct RateLimitB;
|
||||
|
||||
impl RateLimit for RateLimitB {
|
||||
fn capacity(&self) -> usize {
|
||||
10
|
||||
}
|
||||
|
||||
fn refill_duration(&self) -> Duration {
|
||||
Duration::seconds(3)
|
||||
}
|
||||
|
||||
fn db_name(&self) -> &'static str {
|
||||
"rate-limit-b"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod connection_pool;
|
||||
|
||||
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::llm::LlmTokenClaims;
|
||||
use crate::{
|
||||
AppState, Error, Result, auth,
|
||||
@@ -178,15 +179,23 @@ impl Session {
|
||||
Ok(db.has_active_billing_subscription(user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn current_plan(
|
||||
&self,
|
||||
_db: &MutexGuard<'_, DbHandle>,
|
||||
) -> anyhow::Result<proto::Plan> {
|
||||
if self.is_staff() {
|
||||
Ok(proto::Plan::ZedPro)
|
||||
pub async fn current_plan(&self, db: &MutexGuard<'_, DbHandle>) -> anyhow::Result<proto::Plan> {
|
||||
let user_id = self.user_id();
|
||||
|
||||
let subscription = db.get_active_billing_subscription(user_id).await?;
|
||||
let subscription_kind = subscription.and_then(|subscription| subscription.kind);
|
||||
|
||||
let plan = if let Some(subscription_kind) = subscription_kind {
|
||||
match subscription_kind {
|
||||
SubscriptionKind::ZedPro => proto::Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => proto::Plan::ZedProTrial,
|
||||
SubscriptionKind::ZedFree => proto::Plan::Free,
|
||||
}
|
||||
} else {
|
||||
Ok(proto::Plan::Free)
|
||||
}
|
||||
proto::Plan::Free
|
||||
};
|
||||
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
fn user_id(&self) -> UserId {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{Cents, Result, llm};
|
||||
use anyhow::Context as _;
|
||||
use crate::llm::{self, AGENT_EXTENDED_TRIAL_FEATURE_FLAG};
|
||||
use crate::{Cents, Result};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use chrono::{Datelike, Utc};
|
||||
use collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stripe::PriceId;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct StripeBilling {
|
||||
state: RwLock<StripeBillingState>,
|
||||
@@ -17,9 +19,10 @@ pub struct StripeBilling {
|
||||
struct StripeBillingState {
|
||||
meters_by_event_name: HashMap<String, StripeMeter>,
|
||||
price_ids_by_meter_id: HashMap<String, stripe::PriceId>,
|
||||
prices_by_lookup_key: HashMap<String, stripe::Price>,
|
||||
}
|
||||
|
||||
pub struct StripeModel {
|
||||
pub struct StripeModelTokenPrices {
|
||||
input_tokens_price: StripeBillingPrice,
|
||||
input_cache_creation_tokens_price: StripeBillingPrice,
|
||||
input_cache_read_tokens_price: StripeBillingPrice,
|
||||
@@ -62,6 +65,10 @@ impl StripeBilling {
|
||||
}
|
||||
|
||||
for price in prices.data {
|
||||
if let Some(lookup_key) = price.lookup_key.clone() {
|
||||
state.prices_by_lookup_key.insert(lookup_key, price.clone());
|
||||
}
|
||||
|
||||
if let Some(recurring) = price.recurring {
|
||||
if let Some(meter) = recurring.meter {
|
||||
state.price_ids_by_meter_id.insert(meter, price.id);
|
||||
@@ -74,36 +81,49 @@ impl StripeBilling {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn register_model(&self, model: &llm::db::model::Model) -> Result<StripeModel> {
|
||||
pub async fn find_price_by_lookup_key(&self, lookup_key: &str) -> Result<stripe::Price> {
|
||||
self.state
|
||||
.read()
|
||||
.await
|
||||
.prices_by_lookup_key
|
||||
.get(lookup_key)
|
||||
.cloned()
|
||||
.ok_or_else(|| crate::Error::Internal(anyhow!("no price ID found for {lookup_key:?}")))
|
||||
}
|
||||
|
||||
pub async fn register_model_for_token_based_usage(
|
||||
&self,
|
||||
model: &llm::db::model::Model,
|
||||
) -> Result<StripeModelTokenPrices> {
|
||||
let input_tokens_price = self
|
||||
.get_or_insert_price(
|
||||
.get_or_insert_token_price(
|
||||
&format!("model_{}/input_tokens", model.id),
|
||||
&format!("{} (Input Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_input_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
let input_cache_creation_tokens_price = self
|
||||
.get_or_insert_price(
|
||||
.get_or_insert_token_price(
|
||||
&format!("model_{}/input_cache_creation_tokens", model.id),
|
||||
&format!("{} (Input Cache Creation Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_cache_creation_input_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
let input_cache_read_tokens_price = self
|
||||
.get_or_insert_price(
|
||||
.get_or_insert_token_price(
|
||||
&format!("model_{}/input_cache_read_tokens", model.id),
|
||||
&format!("{} (Input Cache Read Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_cache_read_input_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
let output_tokens_price = self
|
||||
.get_or_insert_price(
|
||||
.get_or_insert_token_price(
|
||||
&format!("model_{}/output_tokens", model.id),
|
||||
&format!("{} (Output Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_output_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
Ok(StripeModel {
|
||||
Ok(StripeModelTokenPrices {
|
||||
input_tokens_price,
|
||||
input_cache_creation_tokens_price,
|
||||
input_cache_read_tokens_price,
|
||||
@@ -111,7 +131,7 @@ impl StripeBilling {
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_or_insert_price(
|
||||
async fn get_or_insert_token_price(
|
||||
&self,
|
||||
meter_event_name: &str,
|
||||
price_description: &str,
|
||||
@@ -207,10 +227,43 @@ impl StripeBilling {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn subscribe_to_price(
|
||||
&self,
|
||||
subscription_id: &stripe::SubscriptionId,
|
||||
price_id: &stripe::PriceId,
|
||||
) -> Result<()> {
|
||||
let subscription =
|
||||
stripe::Subscription::retrieve(&self.client, &subscription_id, &[]).await?;
|
||||
|
||||
if subscription_contains_price(&subscription, price_id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
stripe::Subscription::update(
|
||||
&self.client,
|
||||
subscription_id,
|
||||
stripe::UpdateSubscription {
|
||||
items: Some(vec![stripe::UpdateSubscriptionItems {
|
||||
price: Some(price_id.to_string()),
|
||||
..Default::default()
|
||||
}]),
|
||||
trial_settings: Some(stripe::UpdateSubscriptionTrialSettings {
|
||||
end_behavior: stripe::UpdateSubscriptionTrialSettingsEndBehavior {
|
||||
missing_payment_method: stripe::UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel,
|
||||
},
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe_to_model(
|
||||
&self,
|
||||
subscription_id: &stripe::SubscriptionId,
|
||||
model: &StripeModel,
|
||||
model: &StripeModelTokenPrices,
|
||||
) -> Result<()> {
|
||||
let subscription =
|
||||
stripe::Subscription::retrieve(&self.client, &subscription_id, &[]).await?;
|
||||
@@ -268,10 +321,10 @@ impl StripeBilling {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn bill_model_usage(
|
||||
pub async fn bill_model_token_usage(
|
||||
&self,
|
||||
customer_id: &stripe::CustomerId,
|
||||
model: &StripeModel,
|
||||
model: &StripeModelTokenPrices,
|
||||
event: &llm::db::billing_event::Model,
|
||||
) -> Result<()> {
|
||||
let timestamp = Utc::now().timestamp();
|
||||
@@ -343,11 +396,37 @@ impl StripeBilling {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn bill_model_request_usage(
|
||||
&self,
|
||||
customer_id: &stripe::CustomerId,
|
||||
event_name: &str,
|
||||
requests: i32,
|
||||
) -> Result<()> {
|
||||
let timestamp = Utc::now().timestamp();
|
||||
let idempotency_key = Uuid::new_v4();
|
||||
|
||||
StripeMeterEvent::create(
|
||||
&self.client,
|
||||
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 checkout(
|
||||
&self,
|
||||
customer_id: stripe::CustomerId,
|
||||
github_login: &str,
|
||||
model: &StripeModel,
|
||||
model: &StripeModelTokenPrices,
|
||||
success_url: &str,
|
||||
) -> Result<String> {
|
||||
let first_of_next_month = Utc::now()
|
||||
@@ -411,16 +490,36 @@ impl StripeBilling {
|
||||
zed_pro_price_id: PriceId,
|
||||
customer_id: stripe::CustomerId,
|
||||
github_login: &str,
|
||||
feature_flags: Vec<String>,
|
||||
success_url: &str,
|
||||
) -> Result<String> {
|
||||
let eligible_for_extended_trial = feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG);
|
||||
|
||||
let trial_period_days = if eligible_for_extended_trial { 60 } else { 14 };
|
||||
|
||||
let mut subscription_metadata = std::collections::HashMap::new();
|
||||
if eligible_for_extended_trial {
|
||||
subscription_metadata.insert(
|
||||
"promo_feature_flag".to_string(),
|
||||
AGENT_EXTENDED_TRIAL_FEATURE_FLAG.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut params = stripe::CreateCheckoutSession::new();
|
||||
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
|
||||
trial_period_days: Some(14),
|
||||
trial_period_days: Some(trial_period_days),
|
||||
trial_settings: Some(stripe::CreateCheckoutSessionSubscriptionDataTrialSettings {
|
||||
end_behavior: stripe::CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior {
|
||||
missing_payment_method: stripe::CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod::Pause,
|
||||
}
|
||||
}),
|
||||
metadata: if !subscription_metadata.is_empty() {
|
||||
Some(subscription_metadata)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
|
||||
|
||||
@@ -680,6 +680,7 @@ async fn test_collaborating_with_code_actions(
|
||||
editor.toggle_code_actions(
|
||||
&ToggleCodeActions {
|
||||
deployed_from_indicator: None,
|
||||
quick_launch: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
@@ -1544,6 +1545,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: true,
|
||||
show_value_hints: true,
|
||||
edit_debounce_ms: 0,
|
||||
scroll_debounce_ms: 0,
|
||||
show_type_hints: true,
|
||||
@@ -1559,6 +1561,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
show_value_hints: true,
|
||||
enabled: true,
|
||||
edit_debounce_ms: 0,
|
||||
scroll_debounce_ms: 0,
|
||||
@@ -1778,6 +1781,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
show_value_hints: true,
|
||||
enabled: false,
|
||||
edit_debounce_ms: 0,
|
||||
scroll_debounce_ms: 0,
|
||||
@@ -1794,6 +1798,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
show_value_hints: true,
|
||||
enabled: true,
|
||||
edit_debounce_ms: 0,
|
||||
scroll_debounce_ms: 0,
|
||||
|
||||
@@ -1517,10 +1517,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
workspace.leader_for_pane(workspace.active_pane())
|
||||
);
|
||||
let item = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(
|
||||
item.tab_description(0, cx).unwrap(),
|
||||
SharedString::from("w.rs")
|
||||
);
|
||||
assert_eq!(item.tab_content_text(0, cx), SharedString::from("w.rs"));
|
||||
});
|
||||
|
||||
// TODO: in app code, this would be done by the collab_ui.
|
||||
@@ -1546,10 +1543,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
executor.run_until_parked();
|
||||
workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
|
||||
let item = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(
|
||||
item.tab_description(0, cx).unwrap(),
|
||||
SharedString::from("x.rs")
|
||||
);
|
||||
assert_eq!(item.tab_content_text(0, cx), SharedString::from("x.rs"));
|
||||
});
|
||||
|
||||
workspace_a.update_in(cx_a, |workspace, window, cx| {
|
||||
@@ -1564,7 +1558,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
workspace.leader_for_pane(workspace.active_pane())
|
||||
);
|
||||
let item = workspace.active_pane().read(cx).active_item().unwrap();
|
||||
assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs");
|
||||
assert_eq!(item.tab_content_text(0, cx), "x.rs");
|
||||
});
|
||||
|
||||
// b moves to y.rs in b's project, a is still following but can't yet see
|
||||
@@ -1625,10 +1619,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
workspace.leader_for_pane(workspace.active_pane())
|
||||
);
|
||||
let item = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(
|
||||
item.tab_description(0, cx).unwrap(),
|
||||
SharedString::from("y.rs")
|
||||
);
|
||||
assert_eq!(item.tab_content_text(0, cx), SharedString::from("y.rs"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1885,13 +1876,7 @@ fn pane_summaries(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) ->
|
||||
items: pane
|
||||
.items()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| {
|
||||
(
|
||||
ix == active_ix,
|
||||
item.tab_description(0, cx)
|
||||
.map_or(String::new(), |s| s.to_string()),
|
||||
)
|
||||
})
|
||||
.map(|(ix, item)| (ix == active_ix, item.tab_content_text(0, cx).into()))
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
@@ -2179,7 +2164,7 @@ async fn test_following_to_channel_notes_other_workspace(
|
||||
cx_a.run_until_parked();
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
let editor = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
|
||||
assert_eq!(editor.tab_content_text(0, cx), "1.txt");
|
||||
});
|
||||
|
||||
// b joins channel and is following a
|
||||
@@ -2188,7 +2173,7 @@ async fn test_following_to_channel_notes_other_workspace(
|
||||
let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
let editor = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
|
||||
assert_eq!(editor.tab_content_text(0, cx), "1.txt");
|
||||
});
|
||||
|
||||
// a opens a second workspace and the channel notes
|
||||
@@ -2212,13 +2197,13 @@ async fn test_following_to_channel_notes_other_workspace(
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
let editor = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
|
||||
assert_eq!(editor.tab_content_text(0, cx), "1.txt");
|
||||
});
|
||||
|
||||
// b should follow a back
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
let editor = workspace.active_item_as::<Editor>(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
|
||||
assert_eq!(editor.tab_content_text(0, cx), "1.txt");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2238,7 +2223,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
cx_a.run_until_parked();
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
let editor = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
|
||||
assert_eq!(editor.tab_content_text(0, cx), "1.txt");
|
||||
});
|
||||
|
||||
// b joins channel and is following a
|
||||
@@ -2247,7 +2232,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
let editor = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
|
||||
assert_eq!(editor.tab_content_text(0, cx), "1.txt");
|
||||
});
|
||||
|
||||
// stop following
|
||||
@@ -2260,7 +2245,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
let editor = workspace.active_item_as::<Editor>(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
|
||||
assert_eq!(editor.tab_content_text(0, cx), "1.txt");
|
||||
});
|
||||
|
||||
// a opens a file in a new window
|
||||
@@ -2281,12 +2266,12 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
let editor = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "2.js");
|
||||
assert_eq!(editor.tab_content_text(0, cx), "2.js");
|
||||
});
|
||||
|
||||
// b should follow a back
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
let editor = workspace.active_item_as::<Editor>(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "2.js");
|
||||
assert_eq!(editor.tab_content_text(0, cx), "2.js");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1824,6 +1824,8 @@ async fn test_active_call_events(
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
executor.run_until_parked();
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
AppState, Config, RateLimiter,
|
||||
AppState, Config,
|
||||
db::{NewUserParams, UserId, tests::TestDb},
|
||||
executor::Executor,
|
||||
rpc::{CLEANUP_TIMEOUT, Principal, RECONNECT_TIMEOUT, Server, ZedVersion},
|
||||
@@ -517,7 +517,6 @@ impl TestServer {
|
||||
blob_store_client: None,
|
||||
stripe_client: None,
|
||||
stripe_billing: None,
|
||||
rate_limiter: Arc::new(RateLimiter::new(test_db.db().clone())),
|
||||
executor,
|
||||
kinesis_client: None,
|
||||
config: Config {
|
||||
|
||||
@@ -393,6 +393,23 @@ impl ChannelView {
|
||||
buffer.acknowledge_buffer_version(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_channel(&self, cx: &App) -> (SharedString, Option<SharedString>) {
|
||||
if let Some(channel) = self.channel(cx) {
|
||||
let status = match (
|
||||
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
|
||||
self.channel_buffer.read(cx).is_connected(),
|
||||
) {
|
||||
(false, true) => None,
|
||||
(true, true) => Some("read-only"),
|
||||
(_, false) => Some("disconnected"),
|
||||
};
|
||||
|
||||
(channel.name.clone(), status.map(Into::into))
|
||||
} else {
|
||||
("<unknown>".into(), Some("disconnected".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for ChannelView {}
|
||||
@@ -440,26 +457,21 @@ impl Item for ChannelView {
|
||||
Some(Icon::new(icon))
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _: &Window, cx: &App) -> gpui::AnyElement {
|
||||
let (channel_name, status) = if let Some(channel) = self.channel(cx) {
|
||||
let status = match (
|
||||
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
|
||||
self.channel_buffer.read(cx).is_connected(),
|
||||
) {
|
||||
(false, true) => None,
|
||||
(true, true) => Some("read-only"),
|
||||
(_, false) => Some("disconnected"),
|
||||
};
|
||||
|
||||
(channel.name.clone(), status)
|
||||
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
|
||||
let (name, status) = self.get_channel(cx);
|
||||
if let Some(status) = status {
|
||||
format!("{name} - {status}").into()
|
||||
} else {
|
||||
("<unknown>".into(), Some("disconnected"))
|
||||
};
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _: &Window, cx: &App) -> gpui::AnyElement {
|
||||
let (name, status) = self.get_channel(cx);
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(channel_name)
|
||||
Label::new(name)
|
||||
.color(params.text_color())
|
||||
.when(params.preview, |this| this.italic()),
|
||||
)
|
||||
|
||||
@@ -65,6 +65,15 @@ pub fn init(cx: &mut App) {
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<CollabPanel>(window, cx);
|
||||
if let Some(collab_panel) = workspace.panel::<CollabPanel>(cx) {
|
||||
collab_panel.update(cx, |panel, cx| {
|
||||
panel.filter_editor.update(cx, |editor, cx| {
|
||||
if editor.snapshot(window, cx).is_focused() {
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
workspace.register_action(|_, _: &OpenChannelNotes, window, cx| {
|
||||
let channel_id = ActiveCall::global(cx)
|
||||
|
||||
@@ -86,6 +86,8 @@ impl Settings for CollaborationPanelSettings {
|
||||
) -> anyhow::Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
impl Settings for ChatPanelSettings {
|
||||
@@ -99,6 +101,8 @@ impl Settings for ChatPanelSettings {
|
||||
) -> anyhow::Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
impl Settings for NotificationPanelSettings {
|
||||
@@ -112,6 +116,8 @@ impl Settings for NotificationPanelSettings {
|
||||
) -> anyhow::Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
impl Settings for MessageEditorSettings {
|
||||
@@ -125,4 +131,6 @@ impl Settings for MessageEditorSettings {
|
||||
) -> anyhow::Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ command_palette_hooks.workspace = true
|
||||
db.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
picker.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -67,7 +67,12 @@ impl CommandPaletteDB {
|
||||
command_name: impl Into<String>,
|
||||
user_query: impl Into<String>,
|
||||
) -> Result<()> {
|
||||
self.write_command_invocation_internal(command_name.into(), user_query.into())
|
||||
let command_name = command_name.into();
|
||||
let user_query = user_query.into();
|
||||
log::debug!(
|
||||
"Writing command invocation: command_name={command_name}, user_query={user_query}"
|
||||
);
|
||||
self.write_command_invocation_internal(command_name, user_query)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -117,7 +122,7 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_saves_and_retrieves_command_invocation() {
|
||||
let db =
|
||||
CommandPaletteDB(db::open_test_db("test_saves_and_retrieves_command_invocation").await);
|
||||
CommandPaletteDB::open_test_db("test_saves_and_retrieves_command_invocation").await;
|
||||
|
||||
let retrieved_cmd = db.get_last_invoked("editor: backspace").unwrap();
|
||||
|
||||
@@ -137,7 +142,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_gets_usage_history() {
|
||||
let db = CommandPaletteDB(db::open_test_db("test_gets_usage_history").await);
|
||||
let db = CommandPaletteDB::open_test_db("test_gets_usage_history").await;
|
||||
db.write_command_invocation("go to line: toggle", "200")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -162,7 +167,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_lists_ordered_by_usage() {
|
||||
let db = CommandPaletteDB(db::open_test_db("test_lists_ordered_by_usage").await);
|
||||
let db = CommandPaletteDB::open_test_db("test_lists_ordered_by_usage").await;
|
||||
|
||||
let empty_commands = db.list_commands_used();
|
||||
match &empty_commands {
|
||||
@@ -195,7 +200,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_handles_max_invocation_entries() {
|
||||
let db = CommandPaletteDB(db::open_test_db("test_handles_max_invocation_entries").await);
|
||||
let db = CommandPaletteDB::open_test_db("test_handles_max_invocation_entries").await;
|
||||
|
||||
for i in 1..=1001 {
|
||||
db.write_command_invocation("some-command", &i.to_string())
|
||||
|
||||
@@ -21,6 +21,7 @@ component.workspace = true
|
||||
gpui.workspace = true
|
||||
languages.workspace = true
|
||||
notifications.workspace = true
|
||||
log.workspace = true
|
||||
project.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
|
||||
@@ -23,7 +23,7 @@ use project::Project;
|
||||
use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
|
||||
|
||||
use ui_input::SingleLineInput;
|
||||
use workspace::{AppState, ItemId, SerializableItem};
|
||||
use workspace::{AppState, ItemId, SerializableItem, delete_unloaded_items};
|
||||
use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent};
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||
@@ -735,8 +735,8 @@ impl From<ComponentId> for ActivePageId {
|
||||
impl Item for ComponentPreview {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
|
||||
Some("Component Preview".into())
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Component Preview".into()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
@@ -860,11 +860,13 @@ impl SerializableItem for ComponentPreview {
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<()>> {
|
||||
cx.background_spawn(async move {
|
||||
COMPONENT_PREVIEW_DB
|
||||
.delete_unloaded_items(workspace_id, alive_items)
|
||||
.await
|
||||
})
|
||||
delete_unloaded_items(
|
||||
alive_items,
|
||||
workspace_id,
|
||||
"component_previews",
|
||||
&COMPONENT_PREVIEW_DB,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn serialize(
|
||||
|
||||
@@ -23,6 +23,9 @@ impl ComponentPreviewDb {
|
||||
workspace_id: WorkspaceId,
|
||||
active_page_id: String,
|
||||
) -> Result<()> {
|
||||
log::debug!(
|
||||
"Saving active page: item_id={item_id:?}, workspace_id={workspace_id:?}, active_page_id={active_page_id}"
|
||||
);
|
||||
let query = "INSERT INTO component_previews(item_id, workspace_id, active_page_id)
|
||||
VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT DO UPDATE SET
|
||||
@@ -44,30 +47,4 @@ impl ComponentPreviewDb {
|
||||
WHERE item_id = ? AND workspace_id = ?
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_unloaded_items(
|
||||
&self,
|
||||
workspace: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
) -> Result<()> {
|
||||
let placeholders = alive_items
|
||||
.iter()
|
||||
.map(|_| "?")
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ");
|
||||
|
||||
let query = format!(
|
||||
"DELETE FROM component_previews WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
|
||||
);
|
||||
|
||||
self.write(move |conn| {
|
||||
let mut statement = Statement::prepare(conn, query)?;
|
||||
let mut next_index = statement.bind(&workspace, 1)?;
|
||||
for id in alive_items {
|
||||
next_index = statement.bind(&id, next_index)?;
|
||||
}
|
||||
statement.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,4 +58,42 @@ impl Settings for ContextServerSettings {
|
||||
) -> anyhow::Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
|
||||
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
|
||||
// we don't handle "inputs" replacement strings, see perplexity-key in this example:
|
||||
// https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_configuration-example
|
||||
#[derive(Deserialize)]
|
||||
struct VsCodeServerCommand {
|
||||
command: String,
|
||||
args: Option<Vec<String>>,
|
||||
env: Option<HashMap<String, String>>,
|
||||
// note: we don't support envFile and type
|
||||
}
|
||||
impl From<VsCodeServerCommand> for ServerCommand {
|
||||
fn from(cmd: VsCodeServerCommand) -> Self {
|
||||
Self {
|
||||
path: cmd.command,
|
||||
args: cmd.args.unwrap_or_default(),
|
||||
env: cmd.env,
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
|
||||
current
|
||||
.context_servers
|
||||
.extend(mcp.iter().filter_map(|(k, v)| {
|
||||
Some((
|
||||
k.clone().into(),
|
||||
ServerConfig {
|
||||
command: Some(
|
||||
serde_json::from_value::<VsCodeServerCommand>(v.clone())
|
||||
.ok()?
|
||||
.into(),
|
||||
),
|
||||
settings: None,
|
||||
},
|
||||
))
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp-types.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
|
||||
@@ -14,10 +14,16 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::WorktreeId;
|
||||
use smol::{self, fs::File, lock::Mutex};
|
||||
use std::{
|
||||
borrow::Borrow, collections::HashSet, ffi::OsStr, fmt::Debug, net::Ipv4Addr, ops::Deref,
|
||||
path::PathBuf, sync::Arc,
|
||||
borrow::Borrow,
|
||||
collections::HashSet,
|
||||
ffi::OsStr,
|
||||
fmt::Debug,
|
||||
net::Ipv4Addr,
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{DebugTaskDefinition, TcpArgumentsTemplate};
|
||||
use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -109,6 +115,120 @@ impl TcpArguments {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a debuggable binary/process (what process is going to be debugged and with what arguments).
|
||||
///
|
||||
/// We start off with a [DebugScenario], a user-facing type that additionally defines how a debug target is built; once
|
||||
/// an optional build step is completed, we turn it's result into a DebugTaskDefinition by running a locator (or using a user-provided task) and resolving task variables.
|
||||
/// Finally, a [DebugTaskDefinition] has to be turned into a concrete debugger invocation ([DebugAdapterBinary]).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(
|
||||
any(feature = "test-support", test),
|
||||
derive(serde::Deserialize, serde::Serialize)
|
||||
)]
|
||||
pub struct DebugTaskDefinition {
|
||||
pub label: SharedString,
|
||||
pub adapter: SharedString,
|
||||
pub request: DebugRequest,
|
||||
/// Additional initialization arguments to be sent on DAP initialization
|
||||
pub initialize_args: Option<serde_json::Value>,
|
||||
/// Whether to tell the debug adapter to stop on entry
|
||||
pub stop_on_entry: Option<bool>,
|
||||
/// Optional TCP connection information
|
||||
///
|
||||
/// If provided, this will be used to connect to the debug adapter instead of
|
||||
/// spawning a new debug adapter process. This is useful for connecting to a debug adapter
|
||||
/// that is already running or is started by another process.
|
||||
pub tcp_connection: Option<TcpArgumentsTemplate>,
|
||||
}
|
||||
|
||||
impl DebugTaskDefinition {
|
||||
pub fn cwd(&self) -> Option<&Path> {
|
||||
if let DebugRequest::Launch(config) = &self.request {
|
||||
config.cwd.as_ref().map(Path::new)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_scenario(&self) -> DebugScenario {
|
||||
DebugScenario {
|
||||
label: self.label.clone(),
|
||||
adapter: self.adapter.clone(),
|
||||
build: None,
|
||||
request: Some(self.request.clone()),
|
||||
stop_on_entry: self.stop_on_entry,
|
||||
tcp_connection: self.tcp_connection.clone(),
|
||||
initialize_args: self.initialize_args.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::DebugTaskDefinition {
|
||||
proto::DebugTaskDefinition {
|
||||
adapter: self.adapter.to_string(),
|
||||
request: Some(match &self.request {
|
||||
DebugRequest::Launch(config) => {
|
||||
proto::debug_task_definition::Request::DebugLaunchRequest(
|
||||
proto::DebugLaunchRequest {
|
||||
program: config.program.clone(),
|
||||
cwd: config.cwd.as_ref().map(|c| c.to_string_lossy().to_string()),
|
||||
args: config.args.clone(),
|
||||
env: config
|
||||
.env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
},
|
||||
)
|
||||
}
|
||||
DebugRequest::Attach(attach_request) => {
|
||||
proto::debug_task_definition::Request::DebugAttachRequest(
|
||||
proto::DebugAttachRequest {
|
||||
process_id: attach_request.process_id.unwrap_or_default(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}),
|
||||
label: self.label.to_string(),
|
||||
initialize_args: self.initialize_args.as_ref().map(|v| v.to_string()),
|
||||
tcp_connection: self.tcp_connection.as_ref().map(|t| t.to_proto()),
|
||||
stop_on_entry: self.stop_on_entry,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
|
||||
let request = proto
|
||||
.request
|
||||
.ok_or_else(|| anyhow::anyhow!("request is required"))?;
|
||||
Ok(Self {
|
||||
label: proto.label.into(),
|
||||
initialize_args: proto.initialize_args.map(|v| v.into()),
|
||||
tcp_connection: proto
|
||||
.tcp_connection
|
||||
.map(TcpArgumentsTemplate::from_proto)
|
||||
.transpose()?,
|
||||
stop_on_entry: proto.stop_on_entry,
|
||||
adapter: proto.adapter.into(),
|
||||
request: match request {
|
||||
proto::debug_task_definition::Request::DebugAttachRequest(config) => {
|
||||
DebugRequest::Attach(AttachRequest {
|
||||
process_id: Some(config.process_id),
|
||||
})
|
||||
}
|
||||
|
||||
proto::debug_task_definition::Request::DebugLaunchRequest(config) => {
|
||||
DebugRequest::Launch(LaunchRequest {
|
||||
program: config.program,
|
||||
cwd: config.cwd.map(|cwd| cwd.into()),
|
||||
args: config.args,
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DebugAdapterBinary {
|
||||
pub command: String,
|
||||
@@ -284,6 +404,10 @@ pub async fn fetch_latest_adapter_version_from_github(
|
||||
})
|
||||
}
|
||||
|
||||
pub trait InlineValueProvider {
|
||||
fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait DebugAdapter: 'static + Send + Sync {
|
||||
fn name(&self) -> DebugAdapterName;
|
||||
@@ -373,7 +497,12 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary>;
|
||||
|
||||
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeAdapter {}
|
||||
|
||||
@@ -399,6 +528,7 @@ impl FakeAdapter {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
"raw_request": serde_json::to_value(config).unwrap()
|
||||
});
|
||||
let request = match config.request {
|
||||
DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user