Compare commits
223 Commits
example2
...
linux_lice
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f93770f449 | ||
|
|
2b51d99534 | ||
|
|
c623f5a075 | ||
|
|
38ede4bae3 | ||
|
|
fc920bf63d | ||
|
|
04c68dc0cf | ||
|
|
399eced884 | ||
|
|
50f705e779 | ||
|
|
8173534ad5 | ||
|
|
8c03934b26 | ||
|
|
84e4891d54 | ||
|
|
d03d8ccec1 | ||
|
|
4d934f2884 | ||
|
|
e697cf9747 | ||
|
|
e4e455ae6b | ||
|
|
ff4900c942 | ||
|
|
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 | ||
|
|
19fb1e1b0d | ||
|
|
f2cb6d69d5 | ||
|
|
822b6f837d |
13
.github/workflows/eval.yml
vendored
13
.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
|
||||
@@ -60,7 +69,7 @@ jobs:
|
||||
run: cargo build --package=eval
|
||||
|
||||
- name: Run eval
|
||||
run: cargo run --package=eval -- --repetitions=3 --concurrency=1
|
||||
run: cargo run --package=eval -- --repetitions=8 --concurrency=1
|
||||
|
||||
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
|
||||
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
|
||||
|
||||
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`
|
||||
|
||||
@@ -46,5 +46,17 @@
|
||||
"formatter": "auto",
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
"ensure_final_newline_on_save": true,
|
||||
"file_scan_exclusions": ["crates/eval/worktrees/", "crates/eval/repos/"]
|
||||
"file_scan_exclusions": [
|
||||
"crates/eval/worktrees/",
|
||||
"crates/eval/repos/",
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/.jj",
|
||||
"**/CVS",
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
"**/.classpath",
|
||||
"**/.settings"
|
||||
]
|
||||
}
|
||||
|
||||
642
Cargo.lock
generated
642
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
49
Cargo.toml
49
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/prompt_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" }
|
||||
prompt_library = { path = "crates/prompt_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,17 @@ 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"
|
||||
documented = "0.9.1"
|
||||
dotenv = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
@@ -452,8 +453,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 +470,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"] }
|
||||
@@ -500,9 +504,10 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
|
||||
quote = "1.0.9"
|
||||
rand = "0.8.5"
|
||||
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",
|
||||
@@ -514,8 +519,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"
|
||||
@@ -557,15 +562,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" }
|
||||
@@ -573,7 +579,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"
|
||||
@@ -585,15 +590,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",
|
||||
@@ -605,9 +610,8 @@ wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.6.1"
|
||||
zed_llm_client = "0.7.1"
|
||||
zstd = "0.11"
|
||||
metal = "0.29"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
git = "https://github.com/zed-industries/async-stripe"
|
||||
@@ -794,5 +798,6 @@ ignored = [
|
||||
"serde",
|
||||
"component",
|
||||
"linkme",
|
||||
"documented",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
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 |
14
assets/icons/zed_max_mode.svg
Normal file
14
assets/icons/zed_max_mode.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2489_484)">
|
||||
<path d="M11 8.9V11C8.51716 11 7.48284 11 5 11V10.4L11 5.6V5H5V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M1.5 5.5V1.5H5" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M14.5 5.5V1.5H11" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M1.5 10.5V14.5H5" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
<path d="M14.5 10.5V14.5H11" stroke="black" stroke-opacity="0.5" stroke-width="1.5"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2489_484">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 687 B |
@@ -212,7 +212,7 @@
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-k h": "assistant::DeployHistory",
|
||||
"ctrl-k l": "assistant::OpenPromptLibrary",
|
||||
"ctrl-k l": "assistant::OpenRulesLibrary",
|
||||
"new": "assistant::NewChat",
|
||||
"ctrl-t": "assistant::NewChat",
|
||||
"ctrl-n": "assistant::NewChat"
|
||||
@@ -241,15 +241,23 @@
|
||||
"ctrl-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-alt-p": "assistant::OpenPromptLibrary",
|
||||
"ctrl-alt-p": "assistant::OpenRulesLibrary",
|
||||
"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": {
|
||||
@@ -308,9 +316,9 @@
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
"new": "prompt_library::NewPrompt",
|
||||
"ctrl-n": "prompt_library::NewPrompt",
|
||||
"ctrl-shift-s": "prompt_library::ToggleDefaultPrompt"
|
||||
"new": "rules_library::NewRule",
|
||||
"ctrl-n": "rules_library::NewRule",
|
||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -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"
|
||||
}
|
||||
@@ -675,7 +685,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"context": "!ContextEditor > Editor && mode == full",
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"shift-enter": "editor::ExpandExcerpts",
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "prompt_library::NewPrompt",
|
||||
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
|
||||
"cmd-n": "rules_library::NewRule",
|
||||
"cmd-shift-s": "rules_library::ToggleDefaultRule",
|
||||
"cmd-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
@@ -257,7 +257,7 @@
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-k h": "assistant::DeployHistory",
|
||||
"cmd-k l": "assistant::OpenPromptLibrary",
|
||||
"cmd-k l": "assistant::OpenRulesLibrary",
|
||||
"cmd-t": "assistant::NewChat",
|
||||
"cmd-n": "assistant::NewChat"
|
||||
}
|
||||
@@ -286,15 +286,23 @@
|
||||
"cmd-alt-n": "agent::NewTextThread",
|
||||
"cmd-shift-h": "agent::OpenHistory",
|
||||
"cmd-alt-c": "agent::OpenConfiguration",
|
||||
"cmd-alt-p": "assistant::OpenPromptLibrary",
|
||||
"cmd-alt-p": "assistant::OpenRulesLibrary",
|
||||
"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,
|
||||
@@ -738,7 +746,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"context": "!ContextEditor > Editor && mode == full",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
|
||||
@@ -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,
|
||||
|
||||
21
assets/licenses/libXau/COPYING
Normal file
21
assets/licenses/libXau/COPYING
Normal file
@@ -0,0 +1,21 @@
|
||||
Copyright 1988, 1993, 1994, 1998 The Open Group
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and its
|
||||
documentation for any purpose is hereby granted without fee, provided that
|
||||
the above copyright notice appear in all copies and that both that
|
||||
copyright notice and this permission notice appear in supporting
|
||||
documentation.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of The Open Group shall not be
|
||||
used in advertising or otherwise to promote the sale, use or other dealings
|
||||
in this Software without prior written authorization from The Open Group.
|
||||
558
assets/licenses/libbsd/COPYING
Normal file
558
assets/licenses/libbsd/COPYING
Normal file
@@ -0,0 +1,558 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
|
||||
Files:
|
||||
*
|
||||
Copyright:
|
||||
Copyright © 2004-2024 Guillem Jover <guillem@hadrons.org>
|
||||
License: BSD-3-clause
|
||||
|
||||
Files:
|
||||
include/bsd/err.h
|
||||
include/bsd/stdlib.h
|
||||
include/bsd/sys/param.h
|
||||
include/bsd/unistd.h
|
||||
src/bsd_getopt.c
|
||||
src/err.c
|
||||
src/fgetln.c
|
||||
src/progname.c
|
||||
Copyright:
|
||||
Copyright © 2005, 2008-2012, 2019 Guillem Jover <guillem@hadrons.org>
|
||||
Copyright © 2005 Hector Garcia Alvarez
|
||||
Copyright © 2005 Aurelien Jarno
|
||||
Copyright © 2006 Robert Millan
|
||||
Copyright © 2018 Facebook, Inc.
|
||||
License: BSD-3-clause
|
||||
|
||||
Files:
|
||||
include/bsd/netinet/ip_icmp.h
|
||||
include/bsd/sys/bitstring.h
|
||||
include/bsd/sys/queue.h
|
||||
include/bsd/sys/time.h
|
||||
include/bsd/timeconv.h
|
||||
include/bsd/vis.h
|
||||
man/bitstring.3bsd
|
||||
man/errc.3bsd
|
||||
man/explicit_bzero.3bsd
|
||||
man/fgetln.3bsd
|
||||
man/fgetwln.3bsd
|
||||
man/fpurge.3bsd
|
||||
man/funopen.3bsd
|
||||
man/getbsize.3bsd
|
||||
man/heapsort.3bsd
|
||||
man/nlist.3bsd
|
||||
man/pwcache.3bsd
|
||||
man/queue.3bsd
|
||||
man/radixsort.3bsd
|
||||
man/reallocarray.3bsd
|
||||
man/reallocf.3bsd
|
||||
man/setmode.3bsd
|
||||
man/strmode.3bsd
|
||||
man/strnstr.3bsd
|
||||
man/strtoi.3bsd
|
||||
man/strtou.3bsd
|
||||
man/unvis.3bsd
|
||||
man/vis.3bsd
|
||||
man/wcslcpy.3bsd
|
||||
src/getbsize.c
|
||||
src/heapsort.c
|
||||
src/merge.c
|
||||
src/nlist.c
|
||||
src/pwcache.c
|
||||
src/radixsort.c
|
||||
src/setmode.c
|
||||
src/strmode.c
|
||||
src/strnstr.c
|
||||
src/strtoi.c
|
||||
src/strtou.c
|
||||
src/unvis.c
|
||||
Copyright:
|
||||
Copyright © 1980, 1982, 1986, 1989-1994
|
||||
The Regents of the University of California. All rights reserved.
|
||||
Copyright © 1992 Keith Muller.
|
||||
Copyright © 2001 Mike Barcroft <mike@FreeBSD.org>
|
||||
.
|
||||
Some code is derived from software contributed to Berkeley by
|
||||
the American National Standards Committee X3, on Information
|
||||
Processing Systems.
|
||||
.
|
||||
Some code is derived from software contributed to Berkeley by
|
||||
Peter McIlroy.
|
||||
.
|
||||
Some code is derived from software contributed to Berkeley by
|
||||
Ronnie Kon at Mindcraft Inc., Kevin Lew and Elmer Yglesias.
|
||||
.
|
||||
Some code is derived from software contributed to Berkeley by
|
||||
Dave Borman at Cray Research, Inc.
|
||||
.
|
||||
Some code is derived from software contributed to Berkeley by
|
||||
Paul Vixie.
|
||||
.
|
||||
Some code is derived from software contributed to Berkeley by
|
||||
Chris Torek.
|
||||
.
|
||||
Copyright © UNIX System Laboratories, Inc.
|
||||
All or some portions of this file are derived from material licensed
|
||||
to the University of California by American Telephone and Telegraph
|
||||
Co. or Unix System Laboratories, Inc. and are reproduced herein with
|
||||
the permission of UNIX System Laboratories, Inc.
|
||||
License: BSD-3-clause-Regents
|
||||
|
||||
Files:
|
||||
src/vis.c
|
||||
Copyright:
|
||||
Copyright © 1989, 1993
|
||||
The Regents of the University of California. All rights reserved.
|
||||
.
|
||||
Copyright © 1999, 2005 The NetBSD Foundation, Inc.
|
||||
All rights reserved.
|
||||
License: BSD-3-clause-Regents and BSD-2-clause-NetBSD
|
||||
|
||||
Files:
|
||||
include/bsd/libutil.h
|
||||
Copyright:
|
||||
Copyright © 1996 Peter Wemm <peter@FreeBSD.org>.
|
||||
All rights reserved.
|
||||
Copyright © 2002 Networks Associates Technology, Inc.
|
||||
All rights reserved.
|
||||
License: BSD-3-clause-author
|
||||
|
||||
Files:
|
||||
man/timeradd.3bsd
|
||||
Copyright:
|
||||
Copyright © 2009 Jukka Ruohonen <jruohonen@iki.fi>
|
||||
Copyright © 1999 Kelly Yancey <kbyanc@posi.net>
|
||||
All rights reserved.
|
||||
License: BSD-3-clause-John-Birrell
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the author nor the names of any co-contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY JOHN BIRRELL AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
|
||||
Files:
|
||||
man/setproctitle.3bsd
|
||||
Copyright:
|
||||
Copyright © 1995 Peter Wemm <peter@FreeBSD.org>
|
||||
All rights reserved.
|
||||
License: BSD-5-clause-Peter-Wemm
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, is permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice immediately at the beginning of the file, without modification,
|
||||
this list of conditions, and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. This work was done expressly for inclusion into FreeBSD. Other use
|
||||
is permitted provided this notation is included.
|
||||
4. Absolutely no warranty of function or purpose is made by the author
|
||||
Peter Wemm.
|
||||
5. Modifications may be freely made to this file providing the above
|
||||
conditions are met.
|
||||
|
||||
Files:
|
||||
include/bsd/stringlist.h
|
||||
man/arc4random.3bsd
|
||||
man/fmtcheck.3bsd
|
||||
man/humanize_number.3bsd
|
||||
man/stringlist.3bsd
|
||||
man/timeval.3bsd
|
||||
src/fmtcheck.c
|
||||
src/humanize_number.c
|
||||
src/stringlist.c
|
||||
src/strtonum.c
|
||||
Copyright:
|
||||
Copyright © 1994, 1997-2000, 2002, 2008, 2010, 2014
|
||||
The NetBSD Foundation, Inc.
|
||||
Copyright © 2013 John-Mark Gurney <jmg@FreeBSD.org>
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2014 The NetBSD Foundation, Inc.
|
||||
All rights reserved.
|
||||
.
|
||||
Some code was derived from software contributed to The NetBSD Foundation
|
||||
by Taylor R. Campbell.
|
||||
.
|
||||
Some code was contributed to The NetBSD Foundation by Allen Briggs.
|
||||
.
|
||||
Some code was contributed to The NetBSD Foundation by Luke Mewburn.
|
||||
.
|
||||
Some code is derived from software contributed to The NetBSD Foundation
|
||||
by Jason R. Thorpe of the Numerical Aerospace Simulation Facility,
|
||||
NASA Ames Research Center, by Luke Mewburn and by Tomas Svensson.
|
||||
.
|
||||
Some code is derived from software contributed to The NetBSD Foundation
|
||||
by Julio M. Merino Vidal, developed as part of Google's Summer of Code
|
||||
2005 program.
|
||||
.
|
||||
Some code is derived from software contributed to The NetBSD Foundation
|
||||
by Christos Zoulas.
|
||||
.
|
||||
Some code is derived from software contributed to The NetBSD Foundation
|
||||
by Jukka Ruohonen.
|
||||
License: BSD-2-clause-NetBSD
|
||||
|
||||
Files:
|
||||
include/bsd/sys/endian.h
|
||||
man/byteorder.3bsd
|
||||
man/closefrom.3bsd
|
||||
man/expand_number.3bsd
|
||||
man/flopen.3bsd
|
||||
man/getpeereid.3bsd
|
||||
man/pidfile.3bsd
|
||||
src/expand_number.c
|
||||
src/pidfile.c
|
||||
src/reallocf.c
|
||||
src/timeconv.c
|
||||
Copyright:
|
||||
Copyright © 1998, M. Warner Losh <imp@freebsd.org>
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2001 Dima Dorfman.
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2001 FreeBSD Inc.
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2002 Thomas Moestl <tmm@FreeBSD.org>
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2002 Mike Barcroft <mike@FreeBSD.org>
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2005 Pawel Jakub Dawidek <pjd@FreeBSD.org>
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2005 Colin Percival
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2007 Eric Anderson <anderson@FreeBSD.org>
|
||||
Copyright © 2007 Pawel Jakub Dawidek <pjd@FreeBSD.org>
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2007 Dag-Erling Coïdan Smørgrav
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2009 Advanced Computing Technologies LLC
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2011 Guillem Jover <guillem@hadrons.org>
|
||||
License: BSD-2-clause
|
||||
|
||||
Files:
|
||||
src/flopen.c
|
||||
Copyright:
|
||||
Copyright © 2007-2009 Dag-Erling Coïdan Smørgrav
|
||||
All rights reserved.
|
||||
License: BSD-2-clause-verbatim
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer
|
||||
in this position and unchanged.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
|
||||
Files:
|
||||
include/bsd/sys/tree.h
|
||||
man/fparseln.3bsd
|
||||
man/tree.3bsd
|
||||
src/fparseln.c
|
||||
Copyright:
|
||||
Copyright © 1997 Christos Zoulas.
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 2002 Niels Provos <provos@citi.umich.edu>
|
||||
All rights reserved.
|
||||
License: BSD-2-clause-author
|
||||
|
||||
Files:
|
||||
include/bsd/readpassphrase.h
|
||||
man/readpassphrase.3bsd
|
||||
man/strlcpy.3bsd
|
||||
man/strtonum.3bsd
|
||||
src/arc4random.c
|
||||
src/arc4random_linux.h
|
||||
src/arc4random_uniform.c
|
||||
src/arc4random_unix.h
|
||||
src/arc4random_win.h
|
||||
src/closefrom.c
|
||||
src/freezero.c
|
||||
src/getentropy_aix.c
|
||||
src/getentropy_bsd.c
|
||||
src/getentropy_hpux.c
|
||||
src/getentropy_hurd.c
|
||||
src/getentropy_linux.c
|
||||
src/getentropy_osx.c
|
||||
src/getentropy_solaris.c
|
||||
src/getentropy_win.c
|
||||
src/readpassphrase.c
|
||||
src/reallocarray.c
|
||||
src/recallocarray.c
|
||||
src/strlcat.c
|
||||
src/strlcpy.c
|
||||
test/explicit_bzero.c
|
||||
test/strtonum.c
|
||||
Copyright:
|
||||
Copyright © 2004 Ted Unangst and Todd Miller
|
||||
All rights reserved.
|
||||
.
|
||||
Copyright © 1996 David Mazieres <dm@uun.org>
|
||||
Copyright © 1998, 2000-2002, 2004-2005, 2007, 2010, 2012-2015
|
||||
Todd C. Miller <Todd.Miller@courtesan.com>
|
||||
Copyright © 2004 Ted Unangst
|
||||
Copyright © 2004 Otto Moerbeek <otto@drijf.net>
|
||||
Copyright © 2008 Damien Miller <djm@openbsd.org>
|
||||
Copyright © 2008, 2010-2011, 2016-2017 Otto Moerbeek <otto@drijf.net>
|
||||
Copyright © 2013 Markus Friedl <markus@openbsd.org>
|
||||
Copyright © 2014 Bob Beck <beck@obtuse.com>
|
||||
Copyright © 2014 Brent Cook <bcook@openbsd.org>
|
||||
Copyright © 2014 Pawel Jakub Dawidek <pjd@FreeBSD.org>
|
||||
Copyright © 2014 Theo de Raadt <deraadt@openbsd.org>
|
||||
Copyright © 2014 Google Inc.
|
||||
Copyright © 2015 Michael Felt <aixtools@gmail.com>
|
||||
Copyright © 2015, 2022 Guillem Jover <guillem@hadrons.org>
|
||||
License: ISC
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
Files:
|
||||
src/inet_net_pton.c
|
||||
Copyright:
|
||||
Copyright © 1996 by Internet Software Consortium.
|
||||
License: ISC-Original
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS
|
||||
ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE
|
||||
CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
|
||||
PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
||||
ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
|
||||
SOFTWARE.
|
||||
|
||||
Files:
|
||||
src/setproctitle.c
|
||||
Copyright:
|
||||
Copyright © 2010 William Ahern
|
||||
Copyright © 2012 Guillem Jover <guillem@hadrons.org>
|
||||
License: Expat
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Files:
|
||||
src/explicit_bzero.c
|
||||
src/chacha_private.h
|
||||
Copyright:
|
||||
None
|
||||
License: public-domain
|
||||
Public domain.
|
||||
|
||||
Files:
|
||||
man/mdX.3bsd
|
||||
Copyright:
|
||||
None
|
||||
License: Beerware
|
||||
"THE BEER-WARE LICENSE" (Revision 42):
|
||||
<phk@login.dkuug.dk> wrote this file. As long as you retain this notice you
|
||||
can do whatever you want with this stuff. If we meet some day, and you think
|
||||
this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp
|
||||
|
||||
License: BSD-3-clause-Regents
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the University nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
|
||||
License: BSD-3-clause-author
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, is permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
|
||||
License: BSD-3-clause
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License: BSD-2-clause-NetBSD
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
|
||||
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License: BSD-2-clause-author
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License: BSD-2-clause
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGE.
|
||||
1
assets/licenses/libcrypto
Symbolic link
1
assets/licenses/libcrypto
Symbolic link
@@ -0,0 +1 @@
|
||||
libssl
|
||||
339
assets/licenses/libssl/LICENSE
Normal file
339
assets/licenses/libssl/LICENSE
Normal file
@@ -0,0 +1,339 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{description}
|
||||
Copyright (C) {year} {fullname}
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
{signature of Ty Coon}, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
30
assets/licenses/libxcb/COPYING
Normal file
30
assets/licenses/libxcb/COPYING
Normal file
@@ -0,0 +1,30 @@
|
||||
Copyright (C) 2001-2006 Bart Massey, Jamey Sharp, and Josh Triplett.
|
||||
All Rights Reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall
|
||||
be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the names of the authors
|
||||
or their institutions shall not be used in advertising or
|
||||
otherwise to promote the sale, use or other dealings in this
|
||||
Software without prior written authorization from the
|
||||
authors.
|
||||
1
assets/licenses/libxkbcommon-x11
Symbolic link
1
assets/licenses/libxkbcommon-x11
Symbolic link
@@ -0,0 +1 @@
|
||||
libxkbcommon
|
||||
279
assets/licenses/libxkbcommon/LICENSE
Normal file
279
assets/licenses/libxkbcommon/LICENSE
Normal file
@@ -0,0 +1,279 @@
|
||||
The following is a list of all copyright notices and license statements which
|
||||
appear in the xkbcommon source tree, along with the SPDX short license
|
||||
identifiers which source files use to refer to them. To see which parts are
|
||||
covered by which license and copyright, refer to the source files.
|
||||
|
||||
If making new contributions, the MIT License is vastly preferred.
|
||||
|
||||
All licenses are derivative of the MIT/X11 license, mostly identical other
|
||||
than no-endorsement clauses (e.g. paragraph 4 of The Open Group's license).
|
||||
|
||||
These statements are split into two sections: one for the code compiled and
|
||||
distributed as part of the libxkbcommon shared library and the code component
|
||||
of all tests, and another only for the test data under test/data, which is
|
||||
distributed with the xkbcommon source code, but not installed to the system.
|
||||
|
||||
BEGINNING OF SOFTWARE COPYRIGHT/LICENSE STATEMENTS:
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
SPDX full name: MIT License
|
||||
SPDX short identifier: MIT
|
||||
License text:
|
||||
|
||||
Copyright © 2008-2009 Dan Nicholson <dbn.lists@gmail.com>
|
||||
Copyright © 2009-2012, 2016 Daniel Stone <daniel@fooishbar.org>
|
||||
Copyright © 2012-2014, 2020-2022, 2025 Ran Benita <ran@unusedvar.com>
|
||||
Copyright © 2010-2012, 2016 Intel Corporation
|
||||
Copyright © 2013-2015, 2018-2020 Red Hat, Inc.
|
||||
Copyright © 2023-2024 Pierre Le Marre <dev@wismill.eu>
|
||||
Copyright (c) 1991, Oracle and/or its affiliates.
|
||||
Copyright © 2008-2011 Kristian Høgsberg
|
||||
Copyright © 2010 Francisco Jerez <currojerez@riseup.net>
|
||||
Copyright © 2012 Collabora, Ltd.
|
||||
Copyright © 2015 Kazunobu Kuriyama <kazunobu.kuriyama@nifty.com>
|
||||
Copyright (C) 2011 Joseph Adams <joeyadams3.14159@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
SPDX full name: MIT Open Group variant
|
||||
SPDX short identifier: MIT-open-group
|
||||
License text:
|
||||
|
||||
Copyright 1985, 1987, 1988, 1990, 1998 The Open Group
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the names of the authors or their
|
||||
institutions shall not be used in advertising or otherwise to promote the
|
||||
sale, use or other dealings in this Software without prior written
|
||||
authorization from the authors.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
SPDX full name: Historical Permission Notice and Disclaimer
|
||||
SPDX short identifier: HPND
|
||||
License text:
|
||||
|
||||
Copyright (c) 1993, 1994, 1995, 1996 by Silicon Graphics Computer Systems, Inc.
|
||||
|
||||
Permission to use, copy, modify, and distribute this
|
||||
software and its documentation for any purpose and without
|
||||
fee is hereby granted, provided that the above copyright
|
||||
notice appear in all copies and that both that copyright
|
||||
notice and this permission notice appear in supporting
|
||||
documentation, and that the name of Silicon Graphics not be
|
||||
used in advertising or publicity pertaining to distribution
|
||||
of the software without specific prior written permission.
|
||||
Silicon Graphics makes no representation about the suitability
|
||||
of this software for any purpose. It is provided "as is"
|
||||
without any express or implied warranty.
|
||||
|
||||
SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
|
||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON
|
||||
GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
|
||||
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
||||
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH
|
||||
THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
SPDX full name: Historical Permission Notice and Disclaimer - sell variant
|
||||
SPDX short identifier: HPND-sell-variant
|
||||
License text:
|
||||
|
||||
Copyright 1992 by Oki Technosystems Laboratory, Inc.
|
||||
Copyright 1992 by Fuji Xerox Co., Ltd.
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software
|
||||
and its documentation for any purpose is hereby granted without fee,
|
||||
provided that the above copyright notice appear in all copies and
|
||||
that both that copyright notice and this permission notice appear
|
||||
in supporting documentation, and that the name of Oki Technosystems
|
||||
Laboratory and Fuji Xerox not be used in advertising or publicity
|
||||
pertaining to distribution of the software without specific, written
|
||||
prior permission.
|
||||
Oki Technosystems Laboratory and Fuji Xerox make no representations
|
||||
about the suitability of this software for any purpose. It is provided
|
||||
"as is" without express or implied warranty.
|
||||
|
||||
OKI TECHNOSYSTEMS LABORATORY AND FUJI XEROX DISCLAIM ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL OKI TECHNOSYSTEMS
|
||||
LABORATORY AND FUJI XEROX BE LIABLE FOR ANY SPECIAL, INDIRECT OR
|
||||
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE
|
||||
OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
SPDX full name: <none>
|
||||
SPDX short identifier: LicenseRef-digital-equipment-corporation
|
||||
License text:
|
||||
|
||||
Copyright 1987, 1988 by Digital Equipment Corporation, Maynard, Massachusetts.
|
||||
|
||||
All Rights Reserved
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation, and that the name of Digital not be
|
||||
used in advertising or publicity pertaining to distribution of the
|
||||
software without specific, written prior permission.
|
||||
|
||||
DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
|
||||
ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL
|
||||
DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
|
||||
ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
END OF SOFTWARE COPYRIGHT/LICENSE STATEMENTS
|
||||
|
||||
|
||||
BEGINNING OF LICENSE STATEMENTS ONLY FOR UNDISTRIBUTED DATA FILES IN test/data,
|
||||
derived from xkeyboard-config and libX11 nls/ directory:
|
||||
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
SPDX full name: Historical Permission Notice and Disclaimer - sell variant
|
||||
SPDX short identifier: HPND-sell-variant
|
||||
|
||||
Copyright 1996 by Joseph Moss
|
||||
Copyright (C) 2002-2007 Free Software Foundation, Inc.
|
||||
Copyright (C) Dmitry Golubev <lastguru@mail.ru>, 2003-2004
|
||||
Copyright (C) 2004, Gregory Mokhin <mokhin@bog.msu.ru>
|
||||
Copyright (C) 2006 Erdal Ronahî
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and its
|
||||
documentation for any purpose is hereby granted without fee, provided that
|
||||
the above copyright notice appear in all copies and that both that
|
||||
copyright notice and this permission notice appear in supporting
|
||||
documentation, and that the name of the copyright holder(s) not be used in
|
||||
advertising or publicity pertaining to distribution of the software without
|
||||
specific, written prior permission. The copyright holder(s) makes no
|
||||
representations about the suitability of this software for any purpose. It
|
||||
is provided "as is" without express or implied warranty.
|
||||
|
||||
THE COPYRIGHT HOLDER(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
|
||||
INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
|
||||
EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR
|
||||
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
||||
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
SPDX full name: MIT License
|
||||
SPDX short identifier: MIT
|
||||
|
||||
Copyright 2004-2005 Sun Microsystems, Inc. All rights reserved.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
SPDX full name: X11 License
|
||||
SPDX short identifier: X11
|
||||
License text:
|
||||
|
||||
Copyright (c) 1996 X Consortium
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of the X Consortium shall
|
||||
not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization
|
||||
from the X Consortium.
|
||||
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
SPDX full name: MIT Open Group variant
|
||||
SPDX short identifier: MIT-open-group
|
||||
|
||||
Copyright (C) 2004, 2006 Ævar Arnfjörð Bjarmason <avarab@gmail.com>
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
SPDX full name: <none>
|
||||
SPDX short identifier: MIT-open-group
|
||||
SPDX short identifier: LicenseRef-anton-zinoviev
|
||||
License text:
|
||||
|
||||
Copyright (C) 1999, 2000 by Anton Zinoviev <anton@lml.bas.bg>
|
||||
|
||||
This software may be used, modified, copied, distributed, and sold,
|
||||
in both source and binary form provided that the above copyright
|
||||
and these terms are retained. Under no circumstances is the author
|
||||
responsible for the proper functioning of this software, nor does
|
||||
the author assume any responsibility for damages incurred with its
|
||||
use.
|
||||
|
||||
Permission is granted to anyone to use, distribute and modify
|
||||
this file in any way, provided that the above copyright notice
|
||||
is left intact and the author of the modification summarizes
|
||||
the changes in this header.
|
||||
|
||||
This file is distributed without any expressed or implied warranty.
|
||||
@@ -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
|
||||
|
||||
@@ -27,11 +28,89 @@ If appropriate, use tool calls to explore the current project, which contains th
|
||||
- `{{root_name}}`
|
||||
{{/each}}
|
||||
|
||||
- Bias towards not asking the user for help if you can find the answer yourself.
|
||||
- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.
|
||||
- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
|
||||
{{# if (has_tool 'grep') }}
|
||||
- When looking for symbols in the project, prefer the `grep` tool.
|
||||
- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project.
|
||||
- Bias towards not asking the user for help if you can find the answer yourself.
|
||||
- The user might specify a partial file path. If you don't know the full path, use `find_path` (not `grep`) before you read the file.
|
||||
{{/if}}
|
||||
|
||||
## Code Block Formatting
|
||||
|
||||
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 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.
|
||||
|
||||
@@ -167,7 +167,23 @@
|
||||
// Default: not set, defaults to "bar"
|
||||
"cursor_shape": null,
|
||||
// Determines when the mouse cursor should be hidden in an editor or input box.
|
||||
//
|
||||
// 1. Never hide the mouse cursor:
|
||||
// "never"
|
||||
// 2. Hide only when typing:
|
||||
// "on_typing"
|
||||
// 3. Hide on both typing and cursor movement:
|
||||
// "on_typing_and_movement"
|
||||
"hide_mouse": "on_typing_and_movement",
|
||||
// Determines how snippets are sorted relative to other completion items.
|
||||
//
|
||||
// 1. Place snippets at the top of the completion list:
|
||||
// "top"
|
||||
// 2. Place snippets normally without any preference:
|
||||
// "inline"
|
||||
// 3. Place snippets at the bottom of the completion list:
|
||||
// "bottom"
|
||||
"snippet_sort_order": "inline",
|
||||
// How to highlight the current line in the editor.
|
||||
//
|
||||
// 1. Don't highlight the current line:
|
||||
@@ -210,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
|
||||
@@ -585,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.
|
||||
//
|
||||
@@ -646,7 +669,7 @@
|
||||
"fetch": true,
|
||||
"list_directory": false,
|
||||
"now": true,
|
||||
"path_search": true,
|
||||
"find_path": true,
|
||||
"read_file": true,
|
||||
"grep": true,
|
||||
"thinking": true,
|
||||
@@ -670,7 +693,7 @@
|
||||
"list_directory": true,
|
||||
"move_path": false,
|
||||
"now": false,
|
||||
"path_search": true,
|
||||
"find_path": true,
|
||||
"read_file": true,
|
||||
"grep": true,
|
||||
"rename": false,
|
||||
@@ -679,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
|
||||
@@ -806,7 +834,20 @@
|
||||
// "modal_max_width": "full"
|
||||
//
|
||||
// Default: small
|
||||
"modal_max_width": "small"
|
||||
"modal_max_width": "small",
|
||||
// Determines whether the file finder should skip focus for the active file in search results.
|
||||
// There are 2 possible values:
|
||||
//
|
||||
// 1. true: When searching for files, if the currently active file appears as the first result,
|
||||
// auto-focus will skip it and focus the second result instead.
|
||||
// "skip_focus_for_active_in_search": true
|
||||
//
|
||||
// 2. false: When searching for files, the first result will always receive focus,
|
||||
// even if it's the currently active file.
|
||||
// "skip_focus_for_active_in_search": false
|
||||
//
|
||||
// Default: true
|
||||
"skip_focus_for_active_in_search": true
|
||||
},
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
allow-private-module-inception = true
|
||||
avoid-breaking-exported-api = false
|
||||
ignore-interior-mutability = [
|
||||
# Suppresses clippy::mutable_key_type, which is a false positive as the Eq
|
||||
# and Hash impls do not use fields with interior mutability.
|
||||
"agent::context::AgentContextKey"
|
||||
]
|
||||
|
||||
@@ -28,7 +28,6 @@ async-watch.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
@@ -61,10 +60,12 @@ ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
prompt_library.workspace = true
|
||||
rules_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
ref-cast.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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());
|
||||
@@ -962,11 +967,13 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let prompt_store = None;
|
||||
let thread_store = cx
|
||||
.update(|cx| {
|
||||
ThreadStore::load(
|
||||
project.clone(),
|
||||
cx.new(|_| ToolWorkingSet::default()),
|
||||
prompt_store,
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -39,8 +39,9 @@ use thread::ThreadId;
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
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};
|
||||
|
||||
@@ -49,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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -271,7 +272,7 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
.get(id.as_ref())
|
||||
.and_then(|preset| preset.tools.get(&tool.name))
|
||||
.copied()
|
||||
.unwrap_or(false),
|
||||
.unwrap_or(self.profile.enable_all_context_servers),
|
||||
};
|
||||
|
||||
Some(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,14 @@
|
||||
use crate::context::attach_context_to_message;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::context::ContextLoadResult;
|
||||
use crate::inline_prompt_editor::CodegenStatus;
|
||||
use crate::{context::load_context, context_store::ContextStore};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
use futures::{SinkExt, Stream, StreamExt, channel::mpsc, future::LocalBoxFuture, join};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
|
||||
use futures::{
|
||||
SinkExt, Stream, StreamExt, TryStreamExt as _, channel::mpsc, future::LocalBoxFuture, join,
|
||||
};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task, WeakEntity};
|
||||
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
@@ -14,7 +16,9 @@ use language_model::{
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Rope;
|
||||
use smol::future::FutureExt;
|
||||
use std::{
|
||||
@@ -39,6 +43,8 @@ pub struct BufferCodegen {
|
||||
range: Range<Anchor>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
context_store: Entity<ContextStore>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
pub is_insertion: bool,
|
||||
@@ -50,6 +56,8 @@ impl BufferCodegen {
|
||||
range: Range<Anchor>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
context_store: Entity<ContextStore>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -60,6 +68,8 @@ impl BufferCodegen {
|
||||
range.clone(),
|
||||
false,
|
||||
Some(context_store.clone()),
|
||||
project.clone(),
|
||||
prompt_store.clone(),
|
||||
Some(telemetry.clone()),
|
||||
builder.clone(),
|
||||
cx,
|
||||
@@ -75,6 +85,8 @@ impl BufferCodegen {
|
||||
range,
|
||||
initial_transaction_id,
|
||||
context_store,
|
||||
project,
|
||||
prompt_store,
|
||||
telemetry,
|
||||
builder,
|
||||
};
|
||||
@@ -153,6 +165,8 @@ impl BufferCodegen {
|
||||
self.range.clone(),
|
||||
false,
|
||||
Some(self.context_store.clone()),
|
||||
self.project.clone(),
|
||||
self.prompt_store.clone(),
|
||||
Some(self.telemetry.clone()),
|
||||
self.builder.clone(),
|
||||
cx,
|
||||
@@ -229,13 +243,14 @@ pub struct CodegenAlternative {
|
||||
generation: Task<()>,
|
||||
diff: Diff,
|
||||
context_store: Option<Entity<ContextStore>>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
_subscription: gpui::Subscription,
|
||||
builder: Arc<PromptBuilder>,
|
||||
active: bool,
|
||||
edits: Vec<(Range<Anchor>, String)>,
|
||||
line_operations: Vec<LineOperation>,
|
||||
request: Option<LanguageModelRequest>,
|
||||
elapsed_time: Option<f64>,
|
||||
completion: Option<String>,
|
||||
pub message_id: Option<String>,
|
||||
@@ -249,6 +264,8 @@ impl CodegenAlternative {
|
||||
range: Range<Anchor>,
|
||||
active: bool,
|
||||
context_store: Option<Entity<ContextStore>>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -290,6 +307,8 @@ impl CodegenAlternative {
|
||||
generation: Task::ready(()),
|
||||
diff: Diff::default(),
|
||||
context_store,
|
||||
project,
|
||||
prompt_store,
|
||||
telemetry,
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
builder,
|
||||
@@ -297,7 +316,6 @@ impl CodegenAlternative {
|
||||
edits: Vec::new(),
|
||||
line_operations: Vec::new(),
|
||||
range,
|
||||
request: None,
|
||||
elapsed_time: None,
|
||||
completion: None,
|
||||
}
|
||||
@@ -366,16 +384,18 @@ impl CodegenAlternative {
|
||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||
} else {
|
||||
let request = self.build_request(user_prompt, cx)?;
|
||||
self.request = Some(request.clone());
|
||||
|
||||
cx.spawn(async move |_, cx| model.stream_completion_text(request, &cx).await)
|
||||
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
|
||||
.boxed_local()
|
||||
};
|
||||
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_request(&self, user_prompt: String, cx: &mut App) -> Result<LanguageModelRequest> {
|
||||
fn build_request(
|
||||
&self,
|
||||
user_prompt: String,
|
||||
cx: &mut App,
|
||||
) -> Result<Task<LanguageModelRequest>> {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let language = buffer.language_at(self.range.start);
|
||||
let language_name = if let Some(language) = language.as_ref() {
|
||||
@@ -408,30 +428,45 @@ impl CodegenAlternative {
|
||||
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
let context_task = self.context_store.as_ref().map(|context_store| {
|
||||
if let Some(project) = self.project.upgrade() {
|
||||
let context = context_store
|
||||
.read(cx)
|
||||
.context()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
load_context(context, &project, &self.prompt_store, cx)
|
||||
} else {
|
||||
Task::ready(ContextLoadResult::default())
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(context_store) = &self.context_store {
|
||||
attach_context_to_message(
|
||||
&mut request_message,
|
||||
context_store.read(cx).context().iter(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
Ok(cx.spawn(async move |_cx| {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
if let Some(context_task) = context_task {
|
||||
context_task
|
||||
.await
|
||||
.loaded_context
|
||||
.add_to_request_message(&mut request_message);
|
||||
}
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
messages: vec![request_message],
|
||||
})
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
messages: vec![request_message],
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn handle_stream(
|
||||
@@ -469,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);
|
||||
@@ -508,7 +543,9 @@ impl CodegenAlternative {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = StripInvalidSpans::new(stream?.stream);
|
||||
let chunks = StripInvalidSpans::new(
|
||||
stream?.stream.map_err(|error| error.into()),
|
||||
);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
let mut line_diff = LineDiff::default();
|
||||
@@ -1034,6 +1071,7 @@ impl Diff {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use futures::{
|
||||
Stream,
|
||||
stream::{self},
|
||||
@@ -1076,12 +1114,16 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, vec![], cx).await;
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
@@ -1140,12 +1182,16 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, vec![], cx).await;
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
@@ -1207,12 +1253,16 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, vec![], cx).await;
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
@@ -1274,12 +1324,16 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, vec![], cx).await;
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
@@ -1329,12 +1383,16 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, vec![], cx).await;
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
false,
|
||||
None,
|
||||
project.downgrade(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder,
|
||||
cx,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,11 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
pub use completion_provider::ContextPickerCompletionProvider;
|
||||
use editor::display_map::{Crease, FoldId};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
|
||||
use fetch_context_picker::FetchContextPicker;
|
||||
use file_context_picker::FileContextPicker;
|
||||
use file_context_picker::render_file_context_entry;
|
||||
use gpui::{
|
||||
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
|
||||
@@ -20,10 +23,10 @@ use gpui::{
|
||||
use language::Buffer;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::{Entry, ProjectPath};
|
||||
use prompt_store::UserPromptId;
|
||||
use rules_context_picker::RulesContextEntry;
|
||||
use prompt_store::{PromptStore, UserPromptId};
|
||||
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
|
||||
use symbol_context_picker::SymbolContextPicker;
|
||||
use thread_context_picker::{ThreadContextEntry, render_thread_context_entry};
|
||||
use thread_context_picker::{ThreadContextEntry, ThreadContextPicker, render_thread_context_entry};
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
||||
};
|
||||
@@ -32,11 +35,6 @@ use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::AssistantPanel;
|
||||
use crate::context::RULES_ICON;
|
||||
pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
|
||||
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||
use crate::context_picker::rules_context_picker::RulesContextPicker;
|
||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread::ThreadId;
|
||||
use crate::thread_store::ThreadStore;
|
||||
@@ -166,6 +164,7 @@ pub(super) struct ContextPicker {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -193,6 +192,13 @@ impl ContextPicker {
|
||||
)
|
||||
.collect::<Vec<Subscription>>();
|
||||
|
||||
let prompt_store = thread_store.as_ref().and_then(|thread_store| {
|
||||
thread_store
|
||||
.read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
|
||||
ContextPicker {
|
||||
mode: ContextPickerState::Default(ContextMenu::build(
|
||||
window,
|
||||
@@ -202,6 +208,7 @@ impl ContextPicker {
|
||||
workspace,
|
||||
context_store,
|
||||
thread_store,
|
||||
prompt_store,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -226,7 +233,12 @@ impl ContextPicker {
|
||||
.workspace
|
||||
.upgrade()
|
||||
.map(|workspace| {
|
||||
available_context_picker_entries(&self.thread_store, &workspace, cx)
|
||||
available_context_picker_entries(
|
||||
&self.prompt_store,
|
||||
&self.thread_store,
|
||||
&workspace,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -255,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| {
|
||||
@@ -304,10 +316,10 @@ impl ContextPicker {
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Rules => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
if let Some(prompt_store) = self.prompt_store.as_ref() {
|
||||
self.mode = ContextPickerState::Rules(cx.new(|cx| {
|
||||
RulesContextPicker::new(
|
||||
thread_store.clone(),
|
||||
prompt_store.clone(),
|
||||
context_picker.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
@@ -376,7 +388,7 @@ impl ContextPicker {
|
||||
ContextMenuItem::custom_entry(
|
||||
move |_window, cx| {
|
||||
render_file_context_entry(
|
||||
ElementId::NamedInteger("ctx-recent".into(), ix),
|
||||
ElementId::named_usize("ctx-recent", ix),
|
||||
worktree_id,
|
||||
&path,
|
||||
&path_prefix,
|
||||
@@ -526,6 +538,7 @@ enum RecentEntry {
|
||||
}
|
||||
|
||||
fn available_context_picker_entries(
|
||||
prompt_store: &Option<Entity<PromptStore>>,
|
||||
thread_store: &Option<WeakEntity<ThreadStore>>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
@@ -550,6 +563,9 @@ fn available_context_picker_entries(
|
||||
|
||||
if thread_store.is_some() {
|
||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
|
||||
}
|
||||
|
||||
if prompt_store.is_some() {
|
||||
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
|
||||
}
|
||||
|
||||
@@ -585,22 +601,21 @@ fn recent_context_picker_entries(
|
||||
}),
|
||||
);
|
||||
|
||||
let mut current_threads = context_store.read(cx).thread_ids();
|
||||
let current_threads = context_store.read(cx).thread_ids();
|
||||
|
||||
if let Some(active_thread) = workspace
|
||||
let active_thread_id = workspace
|
||||
.panel::<AssistantPanel>(cx)
|
||||
.map(|panel| panel.read(cx).active_thread(cx))
|
||||
{
|
||||
current_threads.insert(active_thread.read(cx).id().clone());
|
||||
}
|
||||
.map(|panel| panel.read(cx).active_thread(cx).read(cx).id());
|
||||
|
||||
if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
|
||||
recent.extend(
|
||||
thread_store
|
||||
.read(cx)
|
||||
.threads()
|
||||
.reverse_chronological_threads()
|
||||
.into_iter()
|
||||
.filter(|thread| !current_threads.contains(&thread.id))
|
||||
.filter(|thread| {
|
||||
Some(&thread.id) != active_thread_id && !current_threads.contains(&thread.id)
|
||||
})
|
||||
.take(2)
|
||||
.map(|thread| {
|
||||
RecentEntry::Thread(ThreadContextEntry {
|
||||
@@ -622,9 +637,7 @@ fn add_selections_as_context(
|
||||
let selection_ranges = selection_ranges(workspace, cx);
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
for (buffer, range) in selection_ranges {
|
||||
context_store
|
||||
.add_selection(buffer, range, cx)
|
||||
.detach_and_log_err(cx);
|
||||
context_store.add_selection(buffer, range, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,22 +15,21 @@ use itertools::Itertools;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
|
||||
use prompt_store::PromptId;
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
use text::{Anchor, OffsetRangeExt, ToPoint};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::RULES_ICON;
|
||||
use crate::context_picker::file_context_picker::search_files;
|
||||
use crate::context_picker::symbol_context_picker::search_symbols;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread_store::ThreadStore;
|
||||
|
||||
use super::fetch_context_picker::fetch_url_content;
|
||||
use super::file_context_picker::FileMatch;
|
||||
use super::file_context_picker::{FileMatch, search_files};
|
||||
use super::rules_context_picker::{RulesContextEntry, search_rules};
|
||||
use super::symbol_context_picker::SymbolMatch;
|
||||
use super::symbol_context_picker::search_symbols;
|
||||
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
|
||||
use super::{
|
||||
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
|
||||
@@ -38,8 +37,8 @@ use super::{
|
||||
};
|
||||
|
||||
pub(crate) enum Match {
|
||||
Symbol(SymbolMatch),
|
||||
File(FileMatch),
|
||||
Symbol(SymbolMatch),
|
||||
Thread(ThreadMatch),
|
||||
Fetch(SharedString),
|
||||
Rules(RulesContextEntry),
|
||||
@@ -69,6 +68,7 @@ fn search(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
recent_entries: Vec<RecentEntry>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
workspace: Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
@@ -85,6 +85,7 @@ fn search(
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
Some(ContextPickerMode::Symbol) => {
|
||||
let search_symbols_task =
|
||||
search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
@@ -96,6 +97,7 @@ fn search(
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
Some(ContextPickerMode::Thread) => {
|
||||
if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
|
||||
let search_threads_task =
|
||||
@@ -111,6 +113,7 @@ fn search(
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
Some(ContextPickerMode::Fetch) => {
|
||||
if !query.is_empty() {
|
||||
Task::ready(vec![Match::Fetch(query.into())])
|
||||
@@ -118,10 +121,11 @@ fn search(
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
Some(ContextPickerMode::Rules) => {
|
||||
if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
|
||||
if let Some(prompt_store) = prompt_store.as_ref() {
|
||||
let search_rules_task =
|
||||
search_rules(query.clone(), cancellation_flag.clone(), thread_store, cx);
|
||||
search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
|
||||
cx.background_spawn(async move {
|
||||
search_rules_task
|
||||
.await
|
||||
@@ -133,6 +137,7 @@ fn search(
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
None => {
|
||||
if query.is_empty() {
|
||||
let mut matches = recent_entries
|
||||
@@ -163,7 +168,7 @@ fn search(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
matches.extend(
|
||||
available_context_picker_entries(&thread_store, &workspace, cx)
|
||||
available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx)
|
||||
.into_iter()
|
||||
.map(|mode| {
|
||||
Match::Entry(EntryMatch {
|
||||
@@ -180,7 +185,8 @@ fn search(
|
||||
let search_files_task =
|
||||
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
|
||||
let entries = available_context_picker_entries(&thread_store, &workspace, cx);
|
||||
let entries =
|
||||
available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx);
|
||||
let entry_candidates = entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -307,9 +313,11 @@ impl ContextPickerCompletionProvider {
|
||||
move |_, _: &mut Window, cx: &mut App| {
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
for (buffer, range) in &selections {
|
||||
context_store
|
||||
.add_selection(buffer.clone(), range.clone(), cx)
|
||||
.detach_and_log_err(cx)
|
||||
context_store.add_selection(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -437,7 +445,6 @@ impl ContextPickerCompletionProvider {
|
||||
source_range: Range<Anchor>,
|
||||
editor: Entity<Editor>,
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
) -> Completion {
|
||||
let new_text = MentionLink::for_rules(&rules);
|
||||
let new_text_len = new_text.len();
|
||||
@@ -457,29 +464,10 @@ impl ContextPickerCompletionProvider {
|
||||
new_text_len,
|
||||
editor.clone(),
|
||||
move |cx| {
|
||||
let prompt_uuid = rules.prompt_id;
|
||||
let prompt_id = PromptId::User { uuid: prompt_uuid };
|
||||
let context_store = context_store.clone();
|
||||
let Some(prompt_store) = thread_store.read(cx).prompt_store() else {
|
||||
log::error!("Can't add user rules as prompt store is missing.");
|
||||
return;
|
||||
};
|
||||
let prompt_store = prompt_store.read(cx);
|
||||
let Some(metadata) = prompt_store.metadata(prompt_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(title) = metadata.title else {
|
||||
return;
|
||||
};
|
||||
let text_task = prompt_store.load(prompt_id, cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let text = text_task.await?;
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
context_store.add_rules(prompt_uuid, title, text, false, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let user_prompt_id = rules.prompt_id;
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
context_store.add_rules(user_prompt_id, false, cx);
|
||||
});
|
||||
},
|
||||
)),
|
||||
}
|
||||
@@ -516,7 +504,7 @@ impl ContextPickerCompletionProvider {
|
||||
let url_to_fetch = url_to_fetch.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
if context_store.update(cx, |context_store, _| {
|
||||
context_store.includes_url(&url_to_fetch).is_some()
|
||||
context_store.includes_url(&url_to_fetch)
|
||||
})? {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -592,7 +580,7 @@ impl ContextPickerCompletionProvider {
|
||||
move |cx| {
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
let task = if is_directory {
|
||||
context_store.add_directory(project_path.clone(), false, cx)
|
||||
Task::ready(context_store.add_directory(&project_path, false, cx))
|
||||
} else {
|
||||
context_store.add_file_from_path(project_path.clone(), false, cx)
|
||||
};
|
||||
@@ -720,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());
|
||||
@@ -732,11 +720,19 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
cx,
|
||||
);
|
||||
|
||||
let prompt_store = thread_store.as_ref().and_then(|thread_store| {
|
||||
thread_store
|
||||
.read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
|
||||
let search_task = search(
|
||||
mode,
|
||||
query,
|
||||
Arc::<AtomicBool>::default(),
|
||||
recent_entries,
|
||||
prompt_store,
|
||||
thread_store.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
@@ -768,6 +764,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
|
||||
symbol,
|
||||
excerpt_id,
|
||||
@@ -777,6 +774,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
workspace.clone(),
|
||||
cx,
|
||||
),
|
||||
|
||||
Match::Thread(ThreadMatch {
|
||||
thread, is_recent, ..
|
||||
}) => {
|
||||
@@ -791,17 +789,15 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
thread_store,
|
||||
))
|
||||
}
|
||||
Match::Rules(user_rules) => {
|
||||
let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
|
||||
Some(Self::completion_for_rules(
|
||||
user_rules,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
thread_store,
|
||||
))
|
||||
}
|
||||
|
||||
Match::Rules(user_rules) => Some(Self::completion_for_rules(
|
||||
user_rules,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
)),
|
||||
|
||||
Match::Fetch(url) => Some(Self::completion_for_fetch(
|
||||
source_range.clone(),
|
||||
url,
|
||||
@@ -810,6 +806,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
context_store.clone(),
|
||||
http_client.clone(),
|
||||
)),
|
||||
|
||||
Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
|
||||
entry,
|
||||
excerpt_id,
|
||||
@@ -1048,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
|
||||
@@ -227,7 +227,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let added = self.context_store.upgrade().map_or(false, |context_store| {
|
||||
context_store.read(cx).includes_url(&self.url).is_some()
|
||||
context_store.read(cx).includes_url(&self.url)
|
||||
});
|
||||
|
||||
Some(
|
||||
|
||||
@@ -134,9 +134,9 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
if is_directory {
|
||||
context_store.add_directory(project_path, true, cx)
|
||||
Task::ready(context_store.add_directory(&project_path, true, cx))
|
||||
} else {
|
||||
context_store.add_file_from_path(project_path, true, cx)
|
||||
context_store.add_file_from_path(project_path.clone(), true, cx)
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
@@ -169,7 +169,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(render_file_context_entry(
|
||||
ElementId::NamedInteger("file-ctx-picker".into(), ix),
|
||||
ElementId::named_usize("file-ctx-picker", ix),
|
||||
WorktreeId::from_usize(mat.worktree_id),
|
||||
&mat.path,
|
||||
&mat.path_prefix,
|
||||
@@ -325,11 +325,11 @@ pub fn render_file_context_entry(
|
||||
path: path.clone(),
|
||||
};
|
||||
if is_directory {
|
||||
context_store.read(cx).includes_directory(&project_path)
|
||||
} else {
|
||||
context_store
|
||||
.read(cx)
|
||||
.will_include_file_path(&project_path, cx)
|
||||
.path_included_in_directory(&project_path, cx)
|
||||
} else {
|
||||
context_store.read(cx).file_path_included(&project_path, cx)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -357,7 +357,7 @@ pub fn render_file_context_entry(
|
||||
})),
|
||||
)
|
||||
.when_some(added, |el, added| match added {
|
||||
FileInclusion::Direct(_) => el.child(
|
||||
FileInclusion::Direct => el.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_end()
|
||||
@@ -369,9 +369,8 @@ pub fn render_file_context_entry(
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
),
|
||||
FileInclusion::InDirectory(directory_project_path) => {
|
||||
// TODO: Consider using worktree full_path to include worktree name.
|
||||
let directory_path = directory_project_path.path.to_string_lossy().into_owned();
|
||||
FileInclusion::InDirectory { full_path } => {
|
||||
let directory_full_path = full_path.to_string_lossy().into_owned();
|
||||
|
||||
el.child(
|
||||
h_flex()
|
||||
@@ -385,7 +384,7 @@ pub fn render_file_context_entry(
|
||||
)
|
||||
.child(Label::new("Included").size(LabelSize::Small)),
|
||||
)
|
||||
.tooltip(Tooltip::text(format!("in {directory_path}")))
|
||||
.tooltip(Tooltip::text(format!("in {directory_full_path}")))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use prompt_store::{PromptId, UserPromptId};
|
||||
use prompt_store::{PromptId, PromptStore, UserPromptId};
|
||||
use ui::{ListItem, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::context::RULES_ICON;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::{self, ContextStore};
|
||||
use crate::thread_store::ThreadStore;
|
||||
|
||||
pub struct RulesContextPicker {
|
||||
picker: Entity<Picker<RulesContextPickerDelegate>>,
|
||||
@@ -18,13 +17,13 @@ pub struct RulesContextPicker {
|
||||
|
||||
impl RulesContextPicker {
|
||||
pub fn new(
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
prompt_store: Entity<PromptStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let delegate = RulesContextPickerDelegate::new(thread_store, context_picker, context_store);
|
||||
let delegate = RulesContextPickerDelegate::new(prompt_store, context_picker, context_store);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
RulesContextPicker { picker }
|
||||
@@ -50,7 +49,7 @@ pub struct RulesContextEntry {
|
||||
}
|
||||
|
||||
pub struct RulesContextPickerDelegate {
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
prompt_store: Entity<PromptStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
matches: Vec<RulesContextEntry>,
|
||||
@@ -59,12 +58,12 @@ pub struct RulesContextPickerDelegate {
|
||||
|
||||
impl RulesContextPickerDelegate {
|
||||
pub fn new(
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
prompt_store: Entity<PromptStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
) -> Self {
|
||||
RulesContextPickerDelegate {
|
||||
thread_store,
|
||||
prompt_store,
|
||||
context_picker,
|
||||
context_store,
|
||||
matches: Vec::new(),
|
||||
@@ -103,11 +102,12 @@ impl PickerDelegate for RulesContextPickerDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = search_rules(query, Arc::new(AtomicBool::default()), thread_store, cx);
|
||||
let search_task = search_rules(
|
||||
query,
|
||||
Arc::new(AtomicBool::default()),
|
||||
&self.prompt_store,
|
||||
cx,
|
||||
);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = search_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -124,31 +124,11 @@ impl PickerDelegate for RulesContextPickerDelegate {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let prompt_id = entry.prompt_id;
|
||||
|
||||
let load_rules_task = thread_store.update(cx, |thread_store, cx| {
|
||||
thread_store.load_rules(prompt_id, cx)
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (metadata, text) = load_rules_task.await?;
|
||||
let Some(title) = metadata.title else {
|
||||
return Err(anyhow!("Encountered user rule with no title when attempting to add it to agent context."));
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.add_rules(prompt_id, title, text, true, cx)
|
||||
})
|
||||
.ok();
|
||||
self.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.add_rules(entry.prompt_id, true, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
@@ -179,11 +159,10 @@ pub fn render_thread_context_entry(
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
cx: &mut App,
|
||||
) -> Div {
|
||||
let added = context_store.upgrade().map_or(false, |ctx_store| {
|
||||
ctx_store
|
||||
let added = context_store.upgrade().map_or(false, |context_store| {
|
||||
context_store
|
||||
.read(cx)
|
||||
.includes_user_rules(&user_rules.prompt_id)
|
||||
.is_some()
|
||||
.includes_user_rules(user_rules.prompt_id)
|
||||
});
|
||||
|
||||
h_flex()
|
||||
@@ -218,12 +197,9 @@ pub fn render_thread_context_entry(
|
||||
pub(crate) fn search_rules(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
prompt_store: &Entity<PromptStore>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<RulesContextEntry>> {
|
||||
let Some(prompt_store) = thread_store.read(cx).prompt_store() else {
|
||||
return Task::ready(vec![]);
|
||||
};
|
||||
let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
|
||||
cx.background_spawn(async move {
|
||||
search_task
|
||||
|
||||
@@ -10,7 +10,6 @@ use gpui::{
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{DocumentSymbol, Symbol};
|
||||
use text::OffsetRangeExt;
|
||||
use ui::{ListItem, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
@@ -172,10 +171,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
|
||||
let mat = &self.matches[ix];
|
||||
|
||||
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||
render_symbol_context_entry(
|
||||
ElementId::NamedInteger("symbol-ctx-picker".into(), ix),
|
||||
mat,
|
||||
),
|
||||
render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -228,18 +224,16 @@ pub(crate) fn add_symbol(
|
||||
)
|
||||
})?;
|
||||
|
||||
context_store
|
||||
.update(cx, move |context_store, cx| {
|
||||
context_store.add_symbol(
|
||||
buffer,
|
||||
name.into(),
|
||||
range,
|
||||
enclosing_range,
|
||||
remove_if_exists,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
context_store.update(cx, move |context_store, cx| {
|
||||
context_store.add_symbol(
|
||||
buffer,
|
||||
name.into(),
|
||||
range,
|
||||
enclosing_range,
|
||||
remove_if_exists,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -353,38 +347,13 @@ fn compute_symbol_entries(
|
||||
context_store: &ContextStore,
|
||||
cx: &App,
|
||||
) -> Vec<SymbolEntry> {
|
||||
let mut symbol_entries = Vec::with_capacity(symbols.len());
|
||||
for SymbolMatch { symbol, .. } in symbols {
|
||||
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
|
||||
let is_included = if let Some(symbols_for_path) = symbols_for_path {
|
||||
let mut is_included = false;
|
||||
for included_symbol_id in symbols_for_path {
|
||||
if included_symbol_id.name.as_ref() == symbol.name.as_str() {
|
||||
if let Some(buffer) = context_store.buffer_for_symbol(included_symbol_id) {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let included_symbol_range =
|
||||
included_symbol_id.range.to_point_utf16(&snapshot);
|
||||
|
||||
if included_symbol_range.start == symbol.range.start.0
|
||||
&& included_symbol_range.end == symbol.range.end.0
|
||||
{
|
||||
is_included = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is_included
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
symbol_entries.push(SymbolEntry {
|
||||
symbols
|
||||
.into_iter()
|
||||
.map(|SymbolMatch { symbol, .. }| SymbolEntry {
|
||||
is_included: context_store.includes_symbol(&symbol, cx),
|
||||
symbol,
|
||||
is_included,
|
||||
})
|
||||
}
|
||||
symbol_entries
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
|
||||
|
||||
@@ -173,7 +173,7 @@ pub fn render_thread_context_entry(
|
||||
cx: &mut App,
|
||||
) -> Div {
|
||||
let added = context_store.upgrade().map_or(false, |ctx_store| {
|
||||
ctx_store.read(cx).includes_thread(&thread.id).is_some()
|
||||
ctx_store.read(cx).includes_thread(&thread.id)
|
||||
});
|
||||
|
||||
h_flex()
|
||||
@@ -219,7 +219,7 @@ pub(crate) fn search_threads(
|
||||
) -> Task<Vec<ThreadMatch>> {
|
||||
let threads = thread_store
|
||||
.read(cx)
|
||||
.threads()
|
||||
.reverse_chronological_threads()
|
||||
.into_iter()
|
||||
.map(|thread| ThreadContextEntry {
|
||||
id: thread.id,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,9 +12,9 @@ use itertools::Itertools;
|
||||
use language::Buffer;
|
||||
use project::ProjectItem;
|
||||
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::{ContextId, ContextKind};
|
||||
use crate::context::{AgentContextHandle, ContextKind};
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread::Thread;
|
||||
@@ -32,6 +32,7 @@ pub struct ContextStrip {
|
||||
focus_handle: FocusHandle,
|
||||
suggest_context_kind: SuggestContextKind,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
focused_index: Option<usize>,
|
||||
children_bounds: Option<Vec<Bounds<Pixels>>>,
|
||||
@@ -73,12 +74,33 @@ impl ContextStrip {
|
||||
focus_handle,
|
||||
suggest_context_kind,
|
||||
workspace,
|
||||
thread_store,
|
||||
_subscriptions: subscriptions,
|
||||
focused_index: None,
|
||||
children_bounds: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
let prompt_store = self
|
||||
.thread_store
|
||||
.as_ref()
|
||||
.and_then(|thread_store| thread_store.upgrade())
|
||||
.and_then(|thread_store| thread_store.read(cx).prompt_store().as_ref());
|
||||
self.context_store
|
||||
.read(cx)
|
||||
.context()
|
||||
.flat_map(|context| {
|
||||
AddedContext::new_pending(context.clone(), prompt_store, project, cx)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn suggested_context(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
|
||||
match self.suggest_context_kind {
|
||||
SuggestContextKind::File => self.suggested_file(cx),
|
||||
@@ -93,22 +115,19 @@ impl ContextStrip {
|
||||
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
||||
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
|
||||
let active_buffer = active_buffer_entity.read(cx);
|
||||
|
||||
let project_path = active_buffer.project_path(cx)?;
|
||||
|
||||
if self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.will_include_buffer(active_buffer.remote_id(), &project_path)
|
||||
.file_path_included(&project_path, cx)
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let file_name = active_buffer.file()?.file_name(cx);
|
||||
|
||||
let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
|
||||
|
||||
Some(SuggestedContext::File {
|
||||
name: file_name.to_string_lossy().into_owned().into(),
|
||||
buffer: active_buffer_entity.downgrade(),
|
||||
@@ -135,7 +154,6 @@ impl ContextStrip {
|
||||
.context_store
|
||||
.read(cx)
|
||||
.includes_thread(active_thread.id())
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -272,12 +290,12 @@ impl ContextStrip {
|
||||
best.map(|(index, _, _)| index)
|
||||
}
|
||||
|
||||
fn open_context(&mut self, id: ContextId, window: &mut Window, cx: &mut App) {
|
||||
fn open_context(&mut self, context: &AgentContextHandle, window: &mut Window, cx: &mut App) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
crate::active_thread::open_context(id, self.context_store.clone(), workspace, window, cx);
|
||||
crate::active_thread::open_context(context, workspace, window, cx);
|
||||
}
|
||||
|
||||
fn remove_focused_context(
|
||||
@@ -287,17 +305,17 @@ impl ContextStrip {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(index) = self.focused_index {
|
||||
let mut is_empty = false;
|
||||
let added_contexts = self.added_contexts(cx);
|
||||
let Some(context) = added_contexts.get(index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.context_store.update(cx, |this, cx| {
|
||||
if let Some(item) = this.context().get(index) {
|
||||
this.remove_context(item.id(), cx);
|
||||
}
|
||||
|
||||
is_empty = this.context().is_empty();
|
||||
this.remove_context(&context.handle, cx);
|
||||
});
|
||||
|
||||
if is_empty {
|
||||
let is_now_empty = added_contexts.len() == 1;
|
||||
if is_now_empty {
|
||||
cx.emit(ContextStripEvent::BlurredEmpty);
|
||||
} else {
|
||||
self.focused_index = Some(index.saturating_sub(1));
|
||||
@@ -306,49 +324,28 @@ impl ContextStrip {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suggested_focused<T>(&self, context: &Vec<T>) -> bool {
|
||||
fn is_suggested_focused(&self, added_contexts: &Vec<AddedContext>) -> bool {
|
||||
// We only suggest one item after the actual context
|
||||
self.focused_index == Some(context.len())
|
||||
self.focused_index == Some(added_contexts.len())
|
||||
}
|
||||
|
||||
fn accept_suggested_context(
|
||||
&mut self,
|
||||
_: &AcceptSuggestedContext,
|
||||
window: &mut Window,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(suggested) = self.suggested_context(cx) {
|
||||
let context_store = self.context_store.read(cx);
|
||||
|
||||
if self.is_suggested_focused(context_store.context()) {
|
||||
self.add_suggested_context(&suggested, window, cx);
|
||||
if self.is_suggested_focused(&self.added_contexts(cx)) {
|
||||
self.add_suggested_context(&suggested, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_suggested_context(
|
||||
&mut self,
|
||||
suggested: &SuggestedContext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let task = self.context_store.update(cx, |context_store, cx| {
|
||||
context_store.accept_suggested_context(&suggested, cx)
|
||||
fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context<Self>) {
|
||||
self.context_store.update(cx, |context_store, cx| {
|
||||
context_store.add_suggested_context(&suggested, cx)
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await.notify_async_err(cx) {
|
||||
None => {}
|
||||
Some(()) => {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |_, cx| cx.notify())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -361,17 +358,10 @@ impl Focusable for ContextStrip {
|
||||
|
||||
impl Render for ContextStrip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let context_store = self.context_store.read(cx);
|
||||
let context = context_store.context();
|
||||
let context_picker = self.context_picker.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let suggested_context = self.suggested_context(cx);
|
||||
|
||||
let added_contexts = context
|
||||
.iter()
|
||||
.map(|c| AddedContext::new(c, cx))
|
||||
.collect::<Vec<_>>();
|
||||
let added_contexts = self.added_contexts(cx);
|
||||
let dupe_names = added_contexts
|
||||
.iter()
|
||||
.map(|c| c.name.clone())
|
||||
@@ -380,6 +370,14 @@ impl Render for ContextStrip {
|
||||
.filter(|(a, b)| a == b)
|
||||
.map(|(a, _)| a)
|
||||
.collect::<HashSet<SharedString>>();
|
||||
let no_added_context = added_contexts.is_empty();
|
||||
|
||||
let suggested_context = self.suggested_context(cx).map(|suggested_context| {
|
||||
(
|
||||
suggested_context,
|
||||
self.is_suggested_focused(&added_contexts),
|
||||
)
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
@@ -436,7 +434,7 @@ impl Render for ContextStrip {
|
||||
})
|
||||
.with_handle(self.context_picker_menu_handle.clone()),
|
||||
)
|
||||
.when(context.is_empty() && suggested_context.is_none(), {
|
||||
.when(no_added_context && suggested_context.is_none(), {
|
||||
|parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
@@ -466,16 +464,17 @@ impl Render for ContextStrip {
|
||||
.enumerate()
|
||||
.map(|(i, added_context)| {
|
||||
let name = added_context.name.clone();
|
||||
let id = added_context.id;
|
||||
let context = added_context.handle.clone();
|
||||
ContextPill::added(
|
||||
added_context,
|
||||
dupe_names.contains(&name),
|
||||
self.focused_index == Some(i),
|
||||
Some({
|
||||
let context = context.clone();
|
||||
let context_store = self.context_store.clone();
|
||||
Rc::new(cx.listener(move |_this, _event, _window, cx| {
|
||||
context_store.update(cx, |this, cx| {
|
||||
this.remove_context(id, cx);
|
||||
this.remove_context(&context, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}))
|
||||
@@ -484,7 +483,7 @@ impl Render for ContextStrip {
|
||||
.on_click({
|
||||
Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
|
||||
if event.down.click_count > 1 {
|
||||
this.open_context(id, window, cx);
|
||||
this.open_context(&context, window, cx);
|
||||
} else {
|
||||
this.focused_index = Some(i);
|
||||
}
|
||||
@@ -493,22 +492,22 @@ impl Render for ContextStrip {
|
||||
})
|
||||
}),
|
||||
)
|
||||
.when_some(suggested_context, |el, suggested| {
|
||||
.when_some(suggested_context, |el, (suggested, focused)| {
|
||||
el.child(
|
||||
ContextPill::suggested(
|
||||
suggested.name().clone(),
|
||||
suggested.icon_path(),
|
||||
suggested.kind(),
|
||||
self.is_suggested_focused(&context),
|
||||
focused,
|
||||
)
|
||||
.on_click(Rc::new(cx.listener(
|
||||
move |this, _event, window, cx| {
|
||||
this.add_suggested_context(&suggested, window, cx);
|
||||
move |this, _event, _window, cx| {
|
||||
this.add_suggested_context(&suggested, cx);
|
||||
},
|
||||
))),
|
||||
)
|
||||
})
|
||||
.when(!context.is_empty(), {
|
||||
.when(!no_added_context, {
|
||||
move |parent| {
|
||||
parent.child(
|
||||
IconButton::new("remove-all-context", IconName::Eraser)
|
||||
@@ -534,6 +533,7 @@ impl Render for ContextStrip {
|
||||
)
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,52 @@ impl HistoryEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum RecentEntry {
|
||||
Thread(ThreadId, Entity<Thread>),
|
||||
Context(Entity<AssistantContext>),
|
||||
}
|
||||
|
||||
impl PartialEq for RecentEntry {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Thread(l0, _), Self::Thread(r0, _)) => l0 == r0,
|
||||
(Self::Context(l0), Self::Context(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RecentEntry {}
|
||||
|
||||
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 +89,62 @@ 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| {
|
||||
let thread_id = ThreadId::from(id.as_str());
|
||||
thread_store
|
||||
.open_thread(&thread_id, cx)
|
||||
.map_ok(|thread| RecentEntry::Thread(thread_id, 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(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +156,10 @@ impl HistoryStore {
|
||||
return history_entries;
|
||||
}
|
||||
|
||||
for thread in self.thread_store.update(cx, |this, _cx| this.threads()) {
|
||||
for thread in self
|
||||
.thread_store
|
||||
.update(cx, |this, _cx| this.reverse_chronological_threads())
|
||||
{
|
||||
history_entries.push(HistoryEntry::Thread(thread));
|
||||
}
|
||||
|
||||
@@ -66,4 +174,63 @@ 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(id, _) => Some(SerializedRecentEntry::Thread(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_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
|
||||
self.recently_opened_entries.retain(|entry| match entry {
|
||||
RecentEntry::Thread(thread_id, _) if thread_id == &id => false,
|
||||
_ => true,
|
||||
});
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ use project::LspAction;
|
||||
use project::Project;
|
||||
use project::{CodeAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::PromptStore;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
@@ -245,9 +246,13 @@ impl InlineAssistant {
|
||||
.map_or(false, |model| model.provider.is_authenticated(cx))
|
||||
};
|
||||
|
||||
let thread_store = workspace
|
||||
let assistant_panel = workspace
|
||||
.panel::<AssistantPanel>(cx)
|
||||
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
|
||||
.map(|assistant_panel| assistant_panel.read(cx));
|
||||
let prompt_store = assistant_panel
|
||||
.and_then(|assistant_panel| assistant_panel.prompt_store().as_ref().cloned());
|
||||
let thread_store =
|
||||
assistant_panel.map(|assistant_panel| assistant_panel.thread_store().downgrade());
|
||||
|
||||
let handle_assist =
|
||||
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
|
||||
@@ -257,6 +262,7 @@ impl InlineAssistant {
|
||||
&active_editor,
|
||||
cx.entity().downgrade(),
|
||||
workspace.project().downgrade(),
|
||||
prompt_store,
|
||||
thread_store,
|
||||
window,
|
||||
cx,
|
||||
@@ -269,6 +275,7 @@ impl InlineAssistant {
|
||||
&active_terminal,
|
||||
cx.entity().downgrade(),
|
||||
workspace.project().downgrade(),
|
||||
prompt_store,
|
||||
thread_store,
|
||||
window,
|
||||
cx,
|
||||
@@ -323,6 +330,7 @@ impl InlineAssistant {
|
||||
editor: &Entity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -437,6 +445,8 @@ impl InlineAssistant {
|
||||
range.clone(),
|
||||
None,
|
||||
context_store.clone(),
|
||||
project.clone(),
|
||||
prompt_store.clone(),
|
||||
self.telemetry.clone(),
|
||||
self.prompt_builder.clone(),
|
||||
cx,
|
||||
@@ -525,6 +535,7 @@ impl InlineAssistant {
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
focus: bool,
|
||||
workspace: Entity<Workspace>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -543,7 +554,7 @@ impl InlineAssistant {
|
||||
}
|
||||
|
||||
let project = workspace.read(cx).project().downgrade();
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.clone(), thread_store.clone()));
|
||||
|
||||
let codegen = cx.new(|cx| {
|
||||
BufferCodegen::new(
|
||||
@@ -551,6 +562,8 @@ impl InlineAssistant {
|
||||
range.clone(),
|
||||
initial_transaction_id,
|
||||
context_store.clone(),
|
||||
project,
|
||||
prompt_store,
|
||||
self.telemetry.clone(),
|
||||
self.prompt_builder.clone(),
|
||||
cx,
|
||||
@@ -1328,7 +1341,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<InlineAssist>(
|
||||
row_range,
|
||||
cx.theme().status().info_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -1393,7 +1406,7 @@ impl InlineAssistant {
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
cx.theme().status().deleted_background,
|
||||
false,
|
||||
Default::default(),
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
@@ -1789,6 +1802,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
let editor = self.editor.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
let prompt_store = PromptStore::global(cx);
|
||||
window.spawn(cx, async move |cx| {
|
||||
let workspace = workspace.upgrade().context("workspace was released")?;
|
||||
let editor = editor.upgrade().context("editor was released")?;
|
||||
@@ -1829,6 +1843,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
})?
|
||||
.context("invalid range")?;
|
||||
|
||||
let prompt_store = prompt_store.await.ok();
|
||||
cx.update_global(|assistant: &mut InlineAssistant, window, cx| {
|
||||
let assist_id = assistant.suggest_assist(
|
||||
&editor,
|
||||
@@ -1837,6 +1852,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
None,
|
||||
true,
|
||||
workspace,
|
||||
prompt_store,
|
||||
thread_store,
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -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;
|
||||
@@ -13,14 +13,14 @@ use editor::{
|
||||
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer,
|
||||
actions::{MoveDown, MoveUp},
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
|
||||
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;
|
||||
@@ -132,7 +132,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
if error.error_code() == proto::ErrorCode::RateLimitExceeded
|
||||
&& cx.has_flag::<ZedPro>()
|
||||
&& cx.has_flag::<ZedProFeatureFlag>()
|
||||
{
|
||||
el.child(
|
||||
v_flex()
|
||||
@@ -931,7 +931,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
.update(cx, |editor, _| editor.set_read_only(false));
|
||||
}
|
||||
CodegenStatus::Error(error) => {
|
||||
if cx.has_flag::<ZedPro>()
|
||||
if cx.has_flag::<ZedProFeatureFlag>()
|
||||
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
|
||||
&& !dismissed_rate_limit_notice()
|
||||
{
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::assistant_model_selector::ModelType;
|
||||
use crate::context::{AssistantContext, format_context_as_string};
|
||||
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};
|
||||
@@ -11,27 +12,31 @@ use editor::{
|
||||
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode,
|
||||
EditorStyle, MultiBuffer,
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt, NewBillingFeatureFlag};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt as _, future};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, ClipboardEntry, Entity, EventEmitter, Focusable, Subscription,
|
||||
Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage};
|
||||
use language_model::{ConfiguredModel, LanguageModelRequestMessage, MessageContent};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
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, refresh_context_store_text};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::profile_selector::ProfileSelector;
|
||||
use crate::thread::{Thread, TokenUsageRatio};
|
||||
@@ -45,19 +50,20 @@ pub struct MessageEditor {
|
||||
thread: Entity<Thread>,
|
||||
incompatible_tools_state: Entity<IncompatibleToolsState>,
|
||||
editor: Entity<Editor>,
|
||||
#[allow(dead_code)]
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
context_store: Entity<ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
context_strip: Entity<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
last_loaded_context: Option<ContextLoadResult>,
|
||||
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>,
|
||||
}
|
||||
|
||||
@@ -68,6 +74,7 @@ impl MessageEditor {
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
thread: Entity<Thread>,
|
||||
window: &mut Window,
|
||||
@@ -135,16 +142,26 @@ impl MessageEditor {
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
|
||||
cx.subscribe(&editor, |this, _, event, cx| match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
this.message_or_context_changed(true, cx);
|
||||
}
|
||||
EditorEvent::BufferEdited => this.handle_message_changed(cx),
|
||||
_ => {}
|
||||
}),
|
||||
cx.observe(&context_store, |this, _, cx| {
|
||||
this.message_or_context_changed(false, 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(),
|
||||
@@ -152,21 +169,14 @@ impl MessageEditor {
|
||||
incompatible_tools_state: incompatible_tools.clone(),
|
||||
workspace,
|
||||
context_store,
|
||||
prompt_store,
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(
|
||||
fs.clone(),
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
load_context_task: None,
|
||||
last_loaded_context: None,
|
||||
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,
|
||||
@@ -175,6 +185,10 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context_store(&self) -> &Entity<ContextStore> {
|
||||
&self.context_store
|
||||
}
|
||||
|
||||
fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.notify();
|
||||
}
|
||||
@@ -195,6 +209,7 @@ impl MessageEditor {
|
||||
editor.set_mode(EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: false,
|
||||
})
|
||||
} else {
|
||||
editor.set_mode(EditorMode::AutoHeight {
|
||||
@@ -213,6 +228,7 @@ impl MessageEditor {
|
||||
) {
|
||||
self.context_picker_menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
pub fn remove_all_context(
|
||||
&mut self,
|
||||
_: &RemoveAllContext,
|
||||
@@ -228,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;
|
||||
@@ -243,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;
|
||||
};
|
||||
|
||||
@@ -269,71 +285,26 @@ impl MessageEditor {
|
||||
self.last_estimated_token_count.take();
|
||||
cx.emit(MessageEditorEvent::EstimatedTokenCount);
|
||||
|
||||
let refresh_task =
|
||||
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
|
||||
let wait_for_images = self.context_store.read(cx).wait_for_images(cx);
|
||||
|
||||
let thread = self.thread.clone();
|
||||
let context_store = self.context_store.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.reload_context(cx);
|
||||
let window_handle = window.window_handle();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let checkpoint = checkpoint.await.ok();
|
||||
refresh_task.await;
|
||||
wait_for_images.await;
|
||||
cx.spawn(async move |_this, cx| {
|
||||
let (checkpoint, loaded_context) = future::join(checkpoint, context_task).await;
|
||||
let loaded_context = loaded_context.unwrap_or_default();
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
let context = context_store.read(cx).context().clone();
|
||||
thread.insert_user_message(user_message, context, checkpoint, cx);
|
||||
thread.insert_user_message(user_message, loaded_context, checkpoint.ok(), cx);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
let excerpt_ids = context_store
|
||||
.context()
|
||||
.iter()
|
||||
.filter(|ctx| {
|
||||
matches!(
|
||||
ctx,
|
||||
AssistantContext::Selection(_) | AssistantContext::Image(_)
|
||||
)
|
||||
})
|
||||
.map(|ctx| ctx.id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for id in excerpt_ids {
|
||||
context_store.remove_context(id, cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(wait_for_summaries) = context_store
|
||||
.update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
|
||||
.log_err()
|
||||
{
|
||||
this.update(cx, |this, cx| {
|
||||
this.waiting_for_summaries_to_send = true;
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
|
||||
wait_for_summaries.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.waiting_for_summaries_to_send = false;
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
// Send to model after summaries are done
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model, cx);
|
||||
thread.send_to_model(model, Some(window_handle), cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
@@ -341,9 +312,13 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let cancelled = self
|
||||
.thread
|
||||
.update(cx, |thread, cx| thread.cancel_last_completion(cx));
|
||||
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)
|
||||
});
|
||||
|
||||
if cancelled {
|
||||
self.set_editor_is_expanded(false, cx);
|
||||
@@ -400,7 +375,7 @@ impl MessageEditor {
|
||||
|
||||
self.context_store.update(cx, |store, cx| {
|
||||
for image in images {
|
||||
store.add_image(Arc::new(image), cx);
|
||||
store.add_image_instance(Arc::new(image), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -424,6 +399,36 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
if !cx.has_flag::<NewBillingFeatureFlag>() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let thread = self.thread.read(cx);
|
||||
let model = thread.configured_model();
|
||||
if !model?.model.supports_max_mode() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let active_completion_mode = thread.completion_mode();
|
||||
|
||||
Some(
|
||||
IconButton::new("max-mode", IconName::ZedMaxMode)
|
||||
.icon_size(IconSize::Small)
|
||||
.toggle_state(active_completion_mode == Some(CompletionMode::Max))
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
this.thread.update(cx, |thread, _cx| {
|
||||
thread.set_completion_mode(match active_completion_mode {
|
||||
Some(CompletionMode::Max) => Some(CompletionMode::Normal),
|
||||
Some(CompletionMode::Normal) | None => Some(CompletionMode::Max),
|
||||
});
|
||||
});
|
||||
}))
|
||||
.tooltip(Tooltip::text("Toggle Max Mode"))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_editor(
|
||||
&self,
|
||||
font_size: Rems,
|
||||
@@ -432,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<_>>()
|
||||
@@ -493,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()
|
||||
@@ -584,6 +588,7 @@ impl MessageEditor {
|
||||
}),
|
||||
)
|
||||
})
|
||||
.children(self.render_max_mode_toggle(cx))
|
||||
.child(self.model_selector.clone())
|
||||
.map({
|
||||
let focus_handle = focus_handle.clone();
|
||||
@@ -636,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(
|
||||
@@ -668,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();
|
||||
@@ -722,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()
|
||||
@@ -759,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")
|
||||
@@ -867,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(
|
||||
@@ -883,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(
|
||||
@@ -1013,18 +1037,49 @@ impl MessageEditor {
|
||||
self.update_token_count_task.is_some()
|
||||
}
|
||||
|
||||
fn reload_context(&mut self, cx: &mut Context<Self>) -> Task<Option<ContextLoadResult>> {
|
||||
let load_task = cx.spawn(async move |this, cx| {
|
||||
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))
|
||||
});
|
||||
load_context(new_context, &this.project, &this.prompt_store, cx)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let result = load_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.last_loaded_context = Some(result);
|
||||
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.load_context_task = Some(load_task.clone());
|
||||
cx.spawn(async move |this, cx| {
|
||||
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;
|
||||
};
|
||||
|
||||
let context_store = self.context_store.clone();
|
||||
let editor = self.editor.clone();
|
||||
let thread = self.thread.clone();
|
||||
|
||||
self.update_token_count_task = Some(cx.spawn(async move |this, cx| {
|
||||
if debounce {
|
||||
@@ -1033,44 +1088,64 @@ impl MessageEditor {
|
||||
.await;
|
||||
}
|
||||
|
||||
let token_count = if let Some(task) = cx.update(|cx| {
|
||||
let context = context_store.read(cx).context().iter();
|
||||
let new_context = thread.read(cx).filter_new_context(context);
|
||||
let context_text =
|
||||
format_context_as_string(new_context, cx).unwrap_or(String::new());
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
let 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 content = context_text + &message_text;
|
||||
if message_text.is_empty()
|
||||
&& loaded_context.map_or(true, |loaded_context| loaded_context.is_empty())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if content.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: vec![content.into()],
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
}],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
};
|
||||
|
||||
Some(default_model.model.count_tokens(request, cx))
|
||||
})? {
|
||||
task.await?
|
||||
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));
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1091,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);
|
||||
@@ -1102,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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ impl TerminalCodegen {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut Context<Self>) {
|
||||
pub fn start(&mut self, prompt_task: Task<LanguageModelRequest>, cx: &mut Context<Self>) {
|
||||
let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
else {
|
||||
@@ -45,6 +45,7 @@ impl TerminalCodegen {
|
||||
self.status = CodegenStatus::Pending;
|
||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||
self.generation = cx.spawn(async move |this, cx| {
|
||||
let prompt = prompt_task.await;
|
||||
let model_telemetry_id = model.telemetry_id();
|
||||
let model_provider_id = model.provider_id();
|
||||
let response = model.stream_completion_text(prompt, &cx).await;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::context::attach_context_to_message;
|
||||
use crate::context::load_context;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::inline_prompt_editor::{
|
||||
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
|
||||
@@ -10,14 +10,14 @@ use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
use fs::Fs;
|
||||
use gpui::{App, Entity, Focusable, Global, Subscription, UpdateGlobal, WeakEntity};
|
||||
use gpui::{App, Entity, Focusable, Global, Subscription, Task, UpdateGlobal, WeakEntity};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
Role, report_assistant_event,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::{PromptBuilder, PromptStore};
|
||||
use std::sync::Arc;
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use terminal_view::TerminalView;
|
||||
@@ -69,6 +69,7 @@ impl TerminalInlineAssistant {
|
||||
terminal_view: &Entity<TerminalView>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: WeakEntity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -109,6 +110,7 @@ impl TerminalInlineAssistant {
|
||||
prompt_editor,
|
||||
workspace.clone(),
|
||||
context_store,
|
||||
prompt_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -196,11 +198,11 @@ impl TerminalInlineAssistant {
|
||||
.log_err();
|
||||
|
||||
let codegen = assist.codegen.clone();
|
||||
let Some(request) = self.request_for_inline_assist(assist_id, cx).log_err() else {
|
||||
let Some(request_task) = self.request_for_inline_assist(assist_id, cx).log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request_task, cx));
|
||||
}
|
||||
|
||||
fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) {
|
||||
@@ -217,7 +219,7 @@ impl TerminalInlineAssistant {
|
||||
&self,
|
||||
assist_id: TerminalInlineAssistId,
|
||||
cx: &mut App,
|
||||
) -> Result<LanguageModelRequest> {
|
||||
) -> Result<Task<LanguageModelRequest>> {
|
||||
let assist = self.assists.get(&assist_id).context("invalid assist")?;
|
||||
|
||||
let shell = std::env::var("SHELL").ok();
|
||||
@@ -246,28 +248,41 @@ impl TerminalInlineAssistant {
|
||||
&latest_output,
|
||||
)?;
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![],
|
||||
cache: false,
|
||||
};
|
||||
let contexts = assist
|
||||
.context_store
|
||||
.read(cx)
|
||||
.context()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let context_load_task = assist.workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project();
|
||||
load_context(contexts, project, &assist.prompt_store, cx)
|
||||
})?;
|
||||
|
||||
attach_context_to_message(
|
||||
&mut request_message,
|
||||
assist.context_store.read(cx).context().iter(),
|
||||
cx,
|
||||
);
|
||||
Ok(cx.background_spawn(async move {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![],
|
||||
cache: false,
|
||||
};
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
context_load_task
|
||||
.await
|
||||
.loaded_context
|
||||
.add_to_request_message(&mut request_message);
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
})
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn finish_assist(
|
||||
@@ -380,6 +395,7 @@ struct TerminalInlineAssist {
|
||||
codegen: Entity<TerminalCodegen>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -390,6 +406,7 @@ impl TerminalInlineAssist {
|
||||
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
@@ -400,6 +417,7 @@ impl TerminalInlineAssist {
|
||||
codegen: codegen.clone(),
|
||||
workspace: workspace.clone(),
|
||||
context_store,
|
||||
prompt_store,
|
||||
_subscriptions: vec![
|
||||
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
|
||||
TerminalInlineAssistant::update_global(cx, |this, cx| {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use fs::Fs;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::future::{self, BoxFuture, Shared};
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
@@ -22,10 +21,10 @@ use gpui::{
|
||||
use heed::Database;
|
||||
use heed::types::SerdeBincode;
|
||||
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
|
||||
use project::{Project, Worktree};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{
|
||||
ProjectContext, PromptBuilder, PromptId, PromptMetadata, PromptStore, PromptsUpdatedEvent,
|
||||
RulesFileContext, UserPromptId, UserRulesContext, WorktreeContext,
|
||||
ProjectContext, PromptBuilder, PromptId, PromptStore, PromptsUpdatedEvent, RulesFileContext,
|
||||
UserRulesContext, WorktreeContext,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
@@ -82,12 +81,11 @@ impl ThreadStore {
|
||||
pub fn load(
|
||||
project: Entity<Project>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
let prompt_store = PromptStore::global(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let prompt_store = prompt_store.await.ok();
|
||||
let (thread_store, ready_rx) = cx.update(|cx| {
|
||||
let mut option_ready_rx = None;
|
||||
let thread_store = cx.new(|cx| {
|
||||
@@ -208,15 +206,15 @@ impl ThreadStore {
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<()> {
|
||||
let project = self.project.read(cx);
|
||||
let worktree_tasks = project
|
||||
let worktrees = self
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.collect::<Vec<_>>();
|
||||
let worktree_tasks = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
Self::load_worktree_info_for_system_prompt(
|
||||
project.fs().clone(),
|
||||
worktree.read(cx),
|
||||
cx,
|
||||
)
|
||||
Self::load_worktree_info_for_system_prompt(worktree, self.project.clone(), cx)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let default_user_rules_task = match prompt_store {
|
||||
@@ -277,13 +275,13 @@ impl ThreadStore {
|
||||
}
|
||||
|
||||
fn load_worktree_info_for_system_prompt(
|
||||
fs: Arc<dyn Fs>,
|
||||
worktree: &Worktree,
|
||||
cx: &App,
|
||||
worktree: Entity<Worktree>,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
|
||||
let root_name = worktree.root_name().into();
|
||||
let root_name = worktree.read(cx).root_name().into();
|
||||
|
||||
let rules_task = Self::load_worktree_rules_file(fs, worktree, cx);
|
||||
let rules_task = Self::load_worktree_rules_file(worktree, project, cx);
|
||||
let Some(rules_task) = rules_task else {
|
||||
return Task::ready((
|
||||
WorktreeContext {
|
||||
@@ -313,33 +311,44 @@ impl ThreadStore {
|
||||
}
|
||||
|
||||
fn load_worktree_rules_file(
|
||||
fs: Arc<dyn Fs>,
|
||||
worktree: &Worktree,
|
||||
cx: &App,
|
||||
worktree: Entity<Worktree>,
|
||||
project: Entity<Project>,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<RulesFileContext>>> {
|
||||
let worktree_ref = worktree.read(cx);
|
||||
let worktree_id = worktree_ref.id();
|
||||
let selected_rules_file = RULES_FILE_NAMES
|
||||
.into_iter()
|
||||
.filter_map(|name| {
|
||||
worktree
|
||||
worktree_ref
|
||||
.entry_for_path(name)
|
||||
.filter(|entry| entry.is_file())
|
||||
.map(|entry| (entry.path.clone(), worktree.absolutize(&entry.path)))
|
||||
.map(|entry| entry.path.clone())
|
||||
})
|
||||
.next();
|
||||
|
||||
// Note that Cline supports `.clinerules` being a directory, but that is not currently
|
||||
// supported. This doesn't seem to occur often in GitHub repositories.
|
||||
selected_rules_file.map(|(path_in_worktree, abs_path)| {
|
||||
let fs = fs.clone();
|
||||
selected_rules_file.map(|path_in_worktree| {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path_in_worktree.clone(),
|
||||
};
|
||||
let buffer_task =
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
let rope_task = cx.spawn(async move |cx| {
|
||||
buffer_task.await?.read_with(cx, |buffer, cx| {
|
||||
let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?;
|
||||
anyhow::Ok((project_entry_id, buffer.as_rope().clone()))
|
||||
})?
|
||||
});
|
||||
// Build a string from the rope on a background thread.
|
||||
cx.background_spawn(async move {
|
||||
let abs_path = abs_path?;
|
||||
let text = fs.load(&abs_path).await.with_context(|| {
|
||||
format!("Failed to load assistant rules file {:?}", abs_path)
|
||||
})?;
|
||||
let (project_entry_id, rope) = rope_task.await?;
|
||||
anyhow::Ok(RulesFileContext {
|
||||
path_in_worktree,
|
||||
abs_path: abs_path.into(),
|
||||
text: text.trim().to_string(),
|
||||
text: rope.to_string().trim().to_string(),
|
||||
project_entry_id: project_entry_id.to_usize(),
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -349,25 +358,8 @@ impl ThreadStore {
|
||||
self.context_server_manager.clone()
|
||||
}
|
||||
|
||||
pub fn prompt_store(&self) -> Option<Entity<PromptStore>> {
|
||||
self.prompt_store.clone()
|
||||
}
|
||||
|
||||
pub fn load_rules(
|
||||
&self,
|
||||
prompt_id: UserPromptId,
|
||||
cx: &App,
|
||||
) -> Task<Result<(PromptMetadata, String)>> {
|
||||
let prompt_id = PromptId::User { uuid: prompt_id };
|
||||
let Some(prompt_store) = self.prompt_store.as_ref() else {
|
||||
return Task::ready(Err(anyhow!("Prompt store unexpectedly missing.")));
|
||||
};
|
||||
let prompt_store = prompt_store.read(cx);
|
||||
let Some(metadata) = prompt_store.metadata(prompt_id) else {
|
||||
return Task::ready(Err(anyhow!("User rules not found in library.")));
|
||||
};
|
||||
let text_task = prompt_store.load(prompt_id, cx);
|
||||
cx.background_spawn(async move { Ok((metadata, text_task.await?)) })
|
||||
pub fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
|
||||
&self.prompt_store
|
||||
}
|
||||
|
||||
pub fn tools(&self) -> Entity<ToolWorkingSet> {
|
||||
@@ -379,16 +371,12 @@ impl ThreadStore {
|
||||
self.threads.len()
|
||||
}
|
||||
|
||||
pub fn threads(&self) -> Vec<SerializedThreadMetadata> {
|
||||
pub fn reverse_chronological_threads(&self) -> Vec<SerializedThreadMetadata> {
|
||||
let mut threads = self.threads.iter().cloned().collect::<Vec<_>>();
|
||||
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at));
|
||||
threads
|
||||
}
|
||||
|
||||
pub fn recent_threads(&self, limit: usize) -> Vec<SerializedThreadMetadata> {
|
||||
self.threads().into_iter().take(limit).collect()
|
||||
}
|
||||
|
||||
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
|
||||
cx.new(|cx| {
|
||||
Thread::new(
|
||||
@@ -516,6 +504,22 @@ impl ThreadStore {
|
||||
);
|
||||
});
|
||||
}
|
||||
// Enable all the tools from all context servers, but disable the ones that are explicitly disabled
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
self.tools.update(cx, |tools, cx| {
|
||||
tools.disable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| (!enabled).then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
self.tools.update(cx, |tools, cx| {
|
||||
@@ -636,15 +640,28 @@ 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 {
|
||||
pub const VERSION: &'static str = "0.1.0";
|
||||
pub const VERSION: &'static str = "0.2.0";
|
||||
|
||||
pub fn from_json(json: &[u8]) -> Result<Self> {
|
||||
let saved_thread_json = serde_json::from_slice::<serde_json::Value>(json)?;
|
||||
match saved_thread_json.get("version") {
|
||||
Some(serde_json::Value::String(version)) => match version.as_str() {
|
||||
SerializedThreadV0_1_0::VERSION => {
|
||||
let saved_thread =
|
||||
serde_json::from_value::<SerializedThreadV0_1_0>(saved_thread_json)?;
|
||||
Ok(saved_thread.upgrade())
|
||||
}
|
||||
SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
|
||||
saved_thread_json,
|
||||
)?),
|
||||
@@ -666,6 +683,38 @@ impl SerializedThread {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SerializedThreadV0_1_0(
|
||||
// The structure did not change, so we are reusing the latest SerializedThread.
|
||||
// When making the next version, make sure this points to SerializedThreadV0_2_0
|
||||
SerializedThread,
|
||||
);
|
||||
|
||||
impl SerializedThreadV0_1_0 {
|
||||
pub const VERSION: &'static str = "0.1.0";
|
||||
|
||||
pub fn upgrade(self) -> SerializedThread {
|
||||
debug_assert_eq!(SerializedThread::VERSION, "0.2.0");
|
||||
|
||||
let mut messages: Vec<SerializedMessage> = Vec::with_capacity(self.0.messages.len());
|
||||
|
||||
for message in self.0.messages {
|
||||
if message.role == Role::User && !message.tool_results.is_empty() {
|
||||
if let Some(last_message) = messages.last_mut() {
|
||||
debug_assert!(last_message.role == Role::Assistant);
|
||||
|
||||
last_message.tool_results = message.tool_results;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
SerializedThread { messages, ..self.0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SerializedMessage {
|
||||
pub id: MessageId,
|
||||
@@ -733,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;
|
||||
@@ -30,7 +30,6 @@ pub struct ToolUse {
|
||||
pub struct ToolUseState {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
|
||||
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
|
||||
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
|
||||
@@ -42,7 +41,6 @@ impl ToolUseState {
|
||||
Self {
|
||||
tools,
|
||||
tool_uses_by_assistant_message: HashMap::default(),
|
||||
tool_uses_by_user_message: HashMap::default(),
|
||||
tool_results: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
tool_result_cards: HashMap::default(),
|
||||
@@ -56,7 +54,6 @@ impl ToolUseState {
|
||||
pub fn from_serialized_messages(
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
messages: &[SerializedMessage],
|
||||
mut filter_by_tool_name: impl FnMut(&str) -> bool,
|
||||
) -> Self {
|
||||
let mut this = Self::new(tools);
|
||||
let mut tool_names_by_id = HashMap::default();
|
||||
@@ -68,10 +65,10 @@ impl ToolUseState {
|
||||
let tool_uses = message
|
||||
.tool_uses
|
||||
.iter()
|
||||
.filter(|tool_use| (filter_by_tool_name)(tool_use.name.as_ref()))
|
||||
.map(|tool_use| LanguageModelToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
raw_input: tool_use.input.to_string(),
|
||||
input: tool_use.input.clone(),
|
||||
is_input_complete: true,
|
||||
})
|
||||
@@ -85,14 +82,6 @@ impl ToolUseState {
|
||||
|
||||
this.tool_uses_by_assistant_message
|
||||
.insert(message.id, tool_uses);
|
||||
}
|
||||
}
|
||||
Role::User => {
|
||||
if !message.tool_results.is_empty() {
|
||||
let tool_uses_by_user_message = this
|
||||
.tool_uses_by_user_message
|
||||
.entry(message.id)
|
||||
.or_default();
|
||||
|
||||
for tool_result in &message.tool_results {
|
||||
let tool_use_id = tool_result.tool_use_id.clone();
|
||||
@@ -101,11 +90,6 @@ impl ToolUseState {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !(filter_by_tool_name)(tool_use.as_ref()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tool_uses_by_user_message.push(tool_use_id.clone());
|
||||
this.tool_results.insert(
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
@@ -118,7 +102,7 @@ impl ToolUseState {
|
||||
}
|
||||
}
|
||||
}
|
||||
Role::System => {}
|
||||
Role::System | Role::User => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,20 +212,26 @@ impl ToolUseState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool_results_for_message(&self, message_id: MessageId) -> Vec<&LanguageModelToolResult> {
|
||||
let empty = Vec::new();
|
||||
pub fn tool_results_for_message(
|
||||
&self,
|
||||
assistant_message_id: MessageId,
|
||||
) -> Vec<&LanguageModelToolResult> {
|
||||
let Some(tool_uses) = self
|
||||
.tool_uses_by_assistant_message
|
||||
.get(&assistant_message_id)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
self.tool_uses_by_user_message
|
||||
.get(&message_id)
|
||||
.unwrap_or(&empty)
|
||||
tool_uses
|
||||
.iter()
|
||||
.filter_map(|tool_use_id| self.tool_results.get(&tool_use_id))
|
||||
.filter_map(|tool_use| self.tool_results.get(&tool_use.id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.tool_uses_by_user_message
|
||||
.get(&message_id)
|
||||
pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool {
|
||||
self.tool_uses_by_assistant_message
|
||||
.get(&assistant_message_id)
|
||||
.map_or(false, |results| !results.is_empty())
|
||||
}
|
||||
|
||||
@@ -293,14 +283,6 @@ impl ToolUseState {
|
||||
self.tool_use_metadata_by_id
|
||||
.insert(tool_use.id.clone(), metadata);
|
||||
|
||||
// The tool use is being requested by the Assistant, so we want to
|
||||
// attach the tool results to the next user message.
|
||||
let next_user_message_id = MessageId(assistant_message_id.0 + 1);
|
||||
self.tool_uses_by_user_message
|
||||
.entry(next_user_message_id)
|
||||
.or_default()
|
||||
.push(tool_use.id.clone());
|
||||
|
||||
PendingToolUseStatus::Idle
|
||||
} else {
|
||||
PendingToolUseStatus::InputStillStreaming
|
||||
@@ -371,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);
|
||||
|
||||
@@ -391,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);
|
||||
|
||||
@@ -466,31 +445,49 @@ impl ToolUseState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attach_tool_results(
|
||||
pub fn has_tool_results(&self, assistant_message_id: MessageId) -> bool {
|
||||
self.tool_uses_by_assistant_message
|
||||
.contains_key(&assistant_message_id)
|
||||
}
|
||||
|
||||
pub fn tool_results_message(
|
||||
&self,
|
||||
message_id: MessageId,
|
||||
request_message: &mut LanguageModelRequestMessage,
|
||||
) {
|
||||
if let Some(tool_uses) = self.tool_uses_by_user_message.get(&message_id) {
|
||||
for tool_use_id in tool_uses {
|
||||
if let Some(tool_result) = self.tool_results.get(tool_use_id) {
|
||||
request_message.content.push(MessageContent::ToolResult(
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name: tool_result.tool_name.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: if tool_result.content.is_empty() {
|
||||
// Surprisingly, the API fails if we return an empty string here.
|
||||
// It thinks we are sending a tool use without a tool result.
|
||||
"<Tool returned an empty string>".into()
|
||||
} else {
|
||||
tool_result.content.clone()
|
||||
},
|
||||
assistant_message_id: MessageId,
|
||||
) -> Option<LanguageModelRequestMessage> {
|
||||
let tool_uses = self
|
||||
.tool_uses_by_assistant_message
|
||||
.get(&assistant_message_id)?;
|
||||
|
||||
if tool_uses.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![],
|
||||
cache: false,
|
||||
};
|
||||
|
||||
for tool_use in tool_uses {
|
||||
if let Some(tool_result) = self.tool_results.get(&tool_use.id) {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::ToolResult(LanguageModelToolResult {
|
||||
tool_use_id: tool_use.id.clone(),
|
||||
tool_name: tool_result.tool_name.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: if tool_result.content.is_empty() {
|
||||
// Surprisingly, the API fails if we return an empty string here.
|
||||
// It thinks we are sending a tool use without a tool result.
|
||||
"<Tool returned an empty string>".into()
|
||||
} else {
|
||||
tool_result.content.clone()
|
||||
},
|
||||
));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Some(request_message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,11 @@ impl LabelCommon for AnimatedLabel {
|
||||
self.base = self.base.buffer_font(cx);
|
||||
self
|
||||
}
|
||||
|
||||
fn inline_code(mut self, cx: &App) -> Self {
|
||||
self.base = self.base.inline_code(cx);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AnimatedLabel {
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
use std::sync::Arc;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use futures::FutureExt;
|
||||
use gpui::{Animation, AnimationExt as _, Image, MouseButton, pulsating_between};
|
||||
use gpui::{ClickEvent, Task};
|
||||
use futures::FutureExt as _;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
|
||||
pulsating_between,
|
||||
};
|
||||
use language_model::LanguageModelImage;
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
|
||||
|
||||
use crate::context::{AssistantContext, ContextId, ContextKind, ImageContext};
|
||||
use crate::context::{
|
||||
AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext,
|
||||
DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext,
|
||||
ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle,
|
||||
SymbolContext, SymbolContextHandle, ThreadContext, ThreadContextHandle,
|
||||
};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub enum ContextPill {
|
||||
@@ -73,9 +82,7 @@ impl ContextPill {
|
||||
|
||||
pub fn id(&self) -> ElementId {
|
||||
match self {
|
||||
Self::Added { context, .. } => {
|
||||
ElementId::NamedInteger("context-pill".into(), context.id.0)
|
||||
}
|
||||
Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
|
||||
Self::Suggested { .. } => "suggested-context-pill".into(),
|
||||
}
|
||||
}
|
||||
@@ -168,16 +175,11 @@ impl RenderOnce for ContextPill {
|
||||
.map(|element| match &context.status {
|
||||
ContextStatus::Ready => element
|
||||
.when_some(
|
||||
context.render_preview.as_ref(),
|
||||
|element, render_preview| {
|
||||
element.hoverable_tooltip({
|
||||
let render_preview = render_preview.clone();
|
||||
move |_, cx| {
|
||||
cx.new(|_| ContextPillPreview {
|
||||
render_preview: render_preview.clone(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
context.render_hover.as_ref(),
|
||||
|element, render_hover| {
|
||||
let render_hover = render_hover.clone();
|
||||
element.hoverable_tooltip(move |window, cx| {
|
||||
render_hover(window, cx)
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -199,14 +201,17 @@ impl RenderOnce for ContextPill {
|
||||
)
|
||||
.when_some(on_remove.as_ref(), |element, on_remove| {
|
||||
element.child(
|
||||
IconButton::new(("remove", context.id.0), IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(Tooltip::text("Remove Context"))
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, window, cx| on_remove(event, window, cx)
|
||||
}),
|
||||
IconButton::new(
|
||||
context.handle.element_id("remove".into()),
|
||||
IconName::Close,
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(Tooltip::text("Remove Context"))
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, window, cx| on_remove(event, window, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(on_click.as_ref(), |element, on_click| {
|
||||
@@ -264,216 +269,441 @@ pub enum ContextStatus {
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct AddedContext {
|
||||
pub id: ContextId,
|
||||
pub handle: AgentContextHandle,
|
||||
pub kind: ContextKind,
|
||||
pub name: SharedString,
|
||||
pub parent: Option<SharedString>,
|
||||
pub tooltip: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
pub status: ContextStatus,
|
||||
pub render_preview: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
|
||||
pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
|
||||
}
|
||||
|
||||
impl AddedContext {
|
||||
pub fn new(context: &AssistantContext, cx: &App) -> AddedContext {
|
||||
/// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
|
||||
/// `None` if `DirectoryContext` or `RulesContext` no longer exist.
|
||||
///
|
||||
/// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
|
||||
pub fn new_pending(
|
||||
handle: AgentContextHandle,
|
||||
prompt_store: Option<&Entity<PromptStore>>,
|
||||
project: &Project,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
match handle {
|
||||
AgentContextHandle::File(handle) => Self::pending_file(handle, cx),
|
||||
AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
|
||||
AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx),
|
||||
AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
|
||||
AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
|
||||
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
|
||||
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
|
||||
AgentContextHandle::Image(handle) => Some(Self::image(handle)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_attached(context: &AgentContext, cx: &App) -> AddedContext {
|
||||
match context {
|
||||
AssistantContext::File(file_context) => {
|
||||
let full_path = file_context.context_buffer.full_path(cx);
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
id: file_context.id,
|
||||
kind: ContextKind::File,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
}
|
||||
}
|
||||
AgentContext::File(context) => Self::attached_file(context, cx),
|
||||
AgentContext::Directory(context) => Self::attached_directory(context),
|
||||
AgentContext::Symbol(context) => Self::attached_symbol(context, cx),
|
||||
AgentContext::Selection(context) => Self::attached_selection(context, cx),
|
||||
AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()),
|
||||
AgentContext::Thread(context) => Self::attached_thread(context),
|
||||
AgentContext::Rules(context) => Self::attached_rules(context),
|
||||
AgentContext::Image(context) => Self::image(context.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
let worktree = directory_context.worktree.read(cx);
|
||||
// If the directory no longer exists, use its last known path.
|
||||
let full_path = worktree
|
||||
.entry_for_id(directory_context.entry_id)
|
||||
.map_or_else(
|
||||
|| directory_context.last_path.clone(),
|
||||
|entry| worktree.full_path(&entry.path).into(),
|
||||
);
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
id: directory_context.id,
|
||||
kind: ContextKind::Directory,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
}
|
||||
}
|
||||
fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let full_path = handle.buffer.read(cx).file()?.full_path(cx);
|
||||
Some(Self::file(handle, &full_path, cx))
|
||||
}
|
||||
|
||||
AssistantContext::Symbol(symbol_context) => AddedContext {
|
||||
id: symbol_context.id,
|
||||
kind: ContextKind::Symbol,
|
||||
name: symbol_context.context_symbol.id.name.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
fn attached_file(context: &FileContext, cx: &App) -> AddedContext {
|
||||
Self::file(context.handle.clone(), &context.full_path, cx)
|
||||
}
|
||||
|
||||
fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
|
||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
kind: ContextKind::File,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::File(handle),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_directory(
|
||||
handle: DirectoryContextHandle,
|
||||
project: &Project,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
|
||||
let entry = worktree.entry_for_id(handle.entry_id)?;
|
||||
let full_path = worktree.full_path(&entry.path);
|
||||
Some(Self::directory(handle, &full_path))
|
||||
}
|
||||
|
||||
fn attached_directory(context: &DirectoryContext) -> AddedContext {
|
||||
Self::directory(context.handle.clone(), &context.full_path)
|
||||
}
|
||||
|
||||
fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
|
||||
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
kind: ContextKind::Directory,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::Directory(handle),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let excerpt =
|
||||
ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Symbol,
|
||||
name: handle.symbol.clone(),
|
||||
parent: Some(excerpt.file_name_and_range.clone()),
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let handle = handle.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(handle.text(cx), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Symbol(handle),
|
||||
})
|
||||
}
|
||||
|
||||
AssistantContext::Selection(selection_context) => {
|
||||
let full_path = selection_context.context_buffer.full_path(cx);
|
||||
let mut full_path_string = full_path.to_string_lossy().into_owned();
|
||||
let mut name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
|
||||
let line_range_text = format!(
|
||||
" ({}-{})",
|
||||
selection_context.line_range.start.row + 1,
|
||||
selection_context.line_range.end.row + 1
|
||||
);
|
||||
|
||||
full_path_string.push_str(&line_range_text);
|
||||
name.push_str(&line_range_text);
|
||||
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
|
||||
AddedContext {
|
||||
id: selection_context.id,
|
||||
kind: ContextKind::Selection,
|
||||
name: name.into(),
|
||||
parent,
|
||||
tooltip: None,
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: Some(Rc::new({
|
||||
let content = selection_context.context_buffer.text.clone();
|
||||
move |_, cx| {
|
||||
div()
|
||||
.id("context-pill-selection-preview")
|
||||
.overflow_scroll()
|
||||
.max_w_128()
|
||||
.max_h_96()
|
||||
.child(Label::new(content.clone()).buffer_font(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
|
||||
id: fetched_url_context.id,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
name: fetched_url_context.url.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext {
|
||||
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
|
||||
AddedContext {
|
||||
kind: ContextKind::Symbol,
|
||||
name: context.handle.symbol.clone(),
|
||||
parent: Some(excerpt.file_name_and_range.clone()),
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Symbol(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::Thread(thread_context) => AddedContext {
|
||||
id: thread_context.id,
|
||||
kind: ContextKind::Thread,
|
||||
name: thread_context.summary(cx),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: if thread_context
|
||||
.thread
|
||||
.read(cx)
|
||||
.is_generating_detailed_summary()
|
||||
{
|
||||
ContextStatus::Loading {
|
||||
message: "Summarizing…".into(),
|
||||
}
|
||||
} else {
|
||||
ContextStatus::Ready
|
||||
fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
|
||||
let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Selection,
|
||||
name: excerpt.file_name_and_range.clone(),
|
||||
parent: excerpt.parent_name.clone(),
|
||||
tooltip: None,
|
||||
icon_path: excerpt.icon_path.clone(),
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let handle = handle.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(handle.text(cx), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Selection(handle),
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext {
|
||||
let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
|
||||
AddedContext {
|
||||
kind: ContextKind::Selection,
|
||||
name: excerpt.file_name_and_range.clone(),
|
||||
parent: excerpt.parent_name.clone(),
|
||||
tooltip: None,
|
||||
icon_path: excerpt.icon_path.clone(),
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
excerpt.hover_view(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Selection(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn fetched_url(context: FetchedUrlContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::FetchedUrl,
|
||||
name: context.url.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::FetchedUrl(context),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::Thread,
|
||||
name: handle.title(cx),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: if handle.thread.read(cx).is_generating_detailed_summary() {
|
||||
ContextStatus::Loading {
|
||||
message: "Summarizing…".into(),
|
||||
}
|
||||
} else {
|
||||
ContextStatus::Ready
|
||||
},
|
||||
render_hover: {
|
||||
let thread = handle.thread.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
let text = thread.read(cx).latest_detailed_summary_or_text();
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Thread(handle),
|
||||
}
|
||||
}
|
||||
|
||||
fn attached_thread(context: &ThreadContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::Thread,
|
||||
name: context.title.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Thread(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_rules(
|
||||
handle: RulesContextHandle,
|
||||
prompt_store: Option<&Entity<PromptStore>>,
|
||||
cx: &App,
|
||||
) -> Option<AddedContext> {
|
||||
let title = prompt_store
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.metadata(handle.prompt_id.into())?
|
||||
.title
|
||||
.unwrap_or_else(|| "Unnamed Rule".into());
|
||||
Some(AddedContext {
|
||||
kind: ContextKind::Rules,
|
||||
name: title.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: None,
|
||||
handle: AgentContextHandle::Rules(handle),
|
||||
})
|
||||
}
|
||||
|
||||
fn attached_rules(context: &RulesContext) -> AddedContext {
|
||||
let title = context
|
||||
.title
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Unnamed Rule".into());
|
||||
AddedContext {
|
||||
kind: ContextKind::Rules,
|
||||
name: title,
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_hover: {
|
||||
let text = context.text.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
handle: AgentContextHandle::Rules(context.handle.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn image(context: ImageContext) -> AddedContext {
|
||||
AddedContext {
|
||||
kind: ContextKind::Image,
|
||||
name: "Image".into(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: match context.status() {
|
||||
ImageStatus::Loading => ContextStatus::Loading {
|
||||
message: "Loading…".into(),
|
||||
},
|
||||
render_preview: None,
|
||||
},
|
||||
|
||||
AssistantContext::Rules(user_rules_context) => AddedContext {
|
||||
id: user_rules_context.id,
|
||||
kind: ContextKind::Rules,
|
||||
name: user_rules_context.title.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: ContextStatus::Ready,
|
||||
render_preview: None,
|
||||
},
|
||||
|
||||
AssistantContext::Image(image_context) => AddedContext {
|
||||
id: image_context.id,
|
||||
kind: ContextKind::Image,
|
||||
name: "Image".into(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: if image_context.is_loading() {
|
||||
ContextStatus::Loading {
|
||||
message: "Loading…".into(),
|
||||
}
|
||||
} else if image_context.is_error() {
|
||||
ContextStatus::Error {
|
||||
message: "Failed to load image".into(),
|
||||
}
|
||||
} else {
|
||||
ContextStatus::Ready
|
||||
ImageStatus::Error => ContextStatus::Error {
|
||||
message: "Failed to load image".into(),
|
||||
},
|
||||
render_preview: Some(Rc::new({
|
||||
let image = image_context.original_image.clone();
|
||||
move |_, _| {
|
||||
ImageStatus::Ready => ContextStatus::Ready,
|
||||
},
|
||||
render_hover: Some(Rc::new({
|
||||
let image = context.original_image.clone();
|
||||
move |_, cx| {
|
||||
let image = image.clone();
|
||||
ContextPillHover::new(cx, move |_, _| {
|
||||
gpui::img(image.clone())
|
||||
.max_w_96()
|
||||
.max_h_96()
|
||||
.into_any_element()
|
||||
}
|
||||
})),
|
||||
},
|
||||
})
|
||||
.into()
|
||||
}
|
||||
})),
|
||||
handle: AgentContextHandle::Image(context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextPillPreview {
|
||||
render_preview: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||
#[derive(Debug, Clone)]
|
||||
struct ContextFileExcerpt {
|
||||
pub file_name_and_range: SharedString,
|
||||
pub full_path_and_range: SharedString,
|
||||
pub parent_name: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Render for ContextPillPreview {
|
||||
impl ContextFileExcerpt {
|
||||
pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
|
||||
let full_path_string = full_path.to_string_lossy().into_owned();
|
||||
let file_name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
|
||||
let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
|
||||
let mut full_path_and_range = full_path_string;
|
||||
full_path_and_range.push_str(&line_range_text);
|
||||
let mut file_name_and_range = file_name;
|
||||
file_name_and_range.push_str(&line_range_text);
|
||||
|
||||
let parent_name = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
|
||||
let icon_path = FileIcons::get_icon(&full_path, cx);
|
||||
|
||||
ContextFileExcerpt {
|
||||
file_name_and_range: file_name_and_range.into(),
|
||||
full_path_and_range: full_path_and_range.into(),
|
||||
parent_name,
|
||||
icon_path,
|
||||
}
|
||||
}
|
||||
|
||||
fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
|
||||
let icon_path = self.icon_path.clone();
|
||||
let full_path_and_range = self.full_path_and_range.clone();
|
||||
ContextPillHover::new(cx, move |_, cx| {
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.children(
|
||||
icon_path
|
||||
.clone()
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(
|
||||
// TODO: make this truncate on the left.
|
||||
Label::new(full_path_and_range.clone())
|
||||
.size(LabelSize::Small)
|
||||
.ml_1(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("context-pill-hover-contents")
|
||||
.overflow_scroll()
|
||||
.max_w_128()
|
||||
.max_h_96()
|
||||
.child(Label::new(text.clone()).buffer_font(cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextPillHover {
|
||||
render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||
}
|
||||
|
||||
impl ContextPillHover {
|
||||
fn new(
|
||||
cx: &mut App,
|
||||
render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|_| Self {
|
||||
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 {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
tooltip_container(window, cx, move |this, window, cx| {
|
||||
this.occlude()
|
||||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child((self.render_preview)(window, cx))
|
||||
.child((self.render_hover)(window, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -488,45 +718,40 @@ impl Component for AddedContext {
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let mut next_context_id = ContextId::zero();
|
||||
let image_ready = (
|
||||
"Ready",
|
||||
AddedContext::new(
|
||||
&AssistantContext::Image(ImageContext {
|
||||
id: ContextId(0),
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
AddedContext::image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
project_path: None,
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
|
||||
}),
|
||||
);
|
||||
|
||||
let image_loading = (
|
||||
"Loading",
|
||||
AddedContext::new(
|
||||
&AssistantContext::Image(ImageContext {
|
||||
id: ContextId(1),
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: cx
|
||||
.background_spawn(async move {
|
||||
smol::Timer::after(Duration::from_secs(60 * 5)).await;
|
||||
Some(LanguageModelImage::empty())
|
||||
})
|
||||
.shared(),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
AddedContext::image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
project_path: None,
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: cx
|
||||
.background_spawn(async move {
|
||||
smol::Timer::after(Duration::from_secs(60 * 5)).await;
|
||||
Some(LanguageModelImage::empty())
|
||||
})
|
||||
.shared(),
|
||||
}),
|
||||
);
|
||||
|
||||
let image_error = (
|
||||
"Error",
|
||||
AddedContext::new(
|
||||
&AssistantContext::Image(ImageContext {
|
||||
id: ContextId(2),
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(None).shared(),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
AddedContext::image(ImageContext {
|
||||
context_id: next_context_id.post_inc(),
|
||||
project_path: None,
|
||||
original_image: Arc::new(Image::empty()),
|
||||
image_task: Task::ready(None).shared(),
|
||||
}),
|
||||
);
|
||||
|
||||
Some(
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
@@ -15,6 +15,7 @@ path = "src/askpass.rs"
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
shlex.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -72,8 +72,7 @@ impl AskPassSession {
|
||||
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
|
||||
let listener =
|
||||
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
|
||||
let zed_path = std::env::current_exe()
|
||||
.context("Failed to figure out current executable path for use in askpass")?;
|
||||
let zed_path = get_shell_safe_zed_path()?;
|
||||
|
||||
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
|
||||
let mut kill_tx = Some(askpass_kill_master_tx);
|
||||
@@ -115,7 +114,7 @@ impl AskPassSession {
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = format!(
|
||||
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
zed_exe = zed_path.display(),
|
||||
zed_exe = zed_path,
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
@@ -161,6 +160,32 @@ impl AskPassSession {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn get_shell_safe_zed_path() -> anyhow::Result<String> {
|
||||
let zed_path = std::env::current_exe()
|
||||
.context("Failed to figure out current executable path for use in askpass")?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// sanity check on unix systems that the path exists and is executable
|
||||
// todo(windows): implement this check for windows (or just use `is-executable` crate)
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
let metadata = std::fs::metadata(&zed_path)
|
||||
.context("Failed to check metadata of Zed executable path for use in askpass")?;
|
||||
let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
|
||||
anyhow::ensure!(
|
||||
is_executable,
|
||||
"Failed to verify Zed executable path for use in askpass"
|
||||
);
|
||||
// As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible
|
||||
// but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other
|
||||
// errors are introduced in the future :(
|
||||
let zed_path_escaped = shlex::try_quote(&zed_path)
|
||||
.context("Failed to shell-escape Zed executable path for use in askpass")?;
|
||||
|
||||
return Ok(zed_path_escaped.to_string());
|
||||
}
|
||||
|
||||
/// The main function for when Zed is running in netcat mode for use in askpass.
|
||||
/// Called from both the remote server binary and the zed binary in their respective main functions.
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -49,7 +49,7 @@ menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
prompt_library.workspace = true
|
||||
rules_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
rope.workspace = true
|
||||
|
||||
@@ -101,7 +101,7 @@ pub fn init(
|
||||
SlashCommandSettings::register(cx);
|
||||
|
||||
assistant_context_editor::init(client.clone(), cx);
|
||||
prompt_library::init(cx);
|
||||
rules_library::init(cx);
|
||||
init_language_model_settings(cx);
|
||||
assistant_slash_command::init(cx);
|
||||
assistant_tool::init(cx);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,17 @@ use gpui::{
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::{PromptBuilder, PromptId, UserPromptId};
|
||||
use prompt_store::{PromptBuilder, UserPromptId};
|
||||
use rules_library::{RulesLibrary, open_rules_library};
|
||||
|
||||
use search::{BufferSearchBar, buffer_search::DivRegistrar};
|
||||
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::*};
|
||||
@@ -44,7 +44,7 @@ use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
pane,
|
||||
};
|
||||
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ShowConfiguration, ToggleFocus};
|
||||
use zed_actions::assistant::{InlineAssist, OpenRulesLibrary, ShowConfiguration, ToggleFocus};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
|
||||
@@ -58,11 +58,11 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(AssistantPanel::show_configuration)
|
||||
.register_action(AssistantPanel::create_new_context)
|
||||
.register_action(AssistantPanel::restart_context_servers)
|
||||
.register_action(|workspace, action: &OpenPromptLibrary, window, cx| {
|
||||
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.deploy_prompt_library(action, window, cx)
|
||||
panel.deploy_rules_library(action, window, cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -273,8 +273,8 @@ impl AssistantPanel {
|
||||
.action("New Chat", Box::new(NewChat))
|
||||
.action("History", Box::new(DeployHistory))
|
||||
.action(
|
||||
"Prompt Library",
|
||||
Box::new(OpenPromptLibrary::default()),
|
||||
"Rules Library",
|
||||
Box::new(OpenRulesLibrary::default()),
|
||||
)
|
||||
.action("Configure", Box::new(ShowConfiguration))
|
||||
.action(zoom_label, Box::new(ToggleZoom))
|
||||
@@ -477,7 +477,7 @@ impl AssistantPanel {
|
||||
{
|
||||
return;
|
||||
}
|
||||
context.custom_summary(new_summary, cx)
|
||||
context.set_custom_summary(new_summary, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -489,8 +489,8 @@ impl AssistantPanel {
|
||||
|
||||
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
|
||||
// the provider, we want to show a nudge to sign in.
|
||||
let show_zed_ai_notice = client_status.is_signed_out()
|
||||
&& model.map_or(true, |model| model.provider.id().0 == ZED_CLOUD_PROVIDER_ID);
|
||||
let show_zed_ai_notice =
|
||||
client_status.is_signed_out() && model.map_or(true, |model| model.is_provided_by_zed());
|
||||
|
||||
self.show_zed_ai_notice = show_zed_ai_notice;
|
||||
cx.notify();
|
||||
@@ -1044,13 +1044,13 @@ impl AssistantPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn deploy_prompt_library(
|
||||
fn deploy_rules_library(
|
||||
&mut self,
|
||||
action: &OpenPromptLibrary,
|
||||
action: &OpenRulesLibrary,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
open_prompt_library(
|
||||
open_rules_library(
|
||||
self.languages.clone(),
|
||||
Box::new(PromptLibraryInlineAssist),
|
||||
Arc::new(|| {
|
||||
@@ -1060,9 +1060,9 @@ impl AssistantPanel {
|
||||
None,
|
||||
))
|
||||
}),
|
||||
action.prompt_to_select.map(|uuid| PromptId::User {
|
||||
uuid: UserPromptId(uuid),
|
||||
}),
|
||||
action
|
||||
.prompt_to_select
|
||||
.map(|uuid| UserPromptId(uuid).into()),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -1081,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<()>> {
|
||||
@@ -1236,7 +1236,7 @@ impl Render for AssistantPanel {
|
||||
this.show_configuration_tab(window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(AssistantPanel::deploy_history))
|
||||
.on_action(cx.listener(AssistantPanel::deploy_prompt_library))
|
||||
.on_action(cx.listener(AssistantPanel::deploy_rules_library))
|
||||
.child(registrar.size_full().child(self.pane.clone()))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -1351,13 +1351,13 @@ impl Focusable for AssistantPanel {
|
||||
|
||||
struct PromptLibraryInlineAssist;
|
||||
|
||||
impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
||||
impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
||||
fn assist(
|
||||
&self,
|
||||
prompt_editor: &Entity<Editor>,
|
||||
initial_prompt: Option<String>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<PromptLibrary>,
|
||||
cx: &mut Context<RulesLibrary>,
|
||||
) {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx)
|
||||
@@ -1392,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<()>> {
|
||||
|
||||
@@ -18,11 +18,11 @@ use editor::{
|
||||
},
|
||||
};
|
||||
use feature_flags::{
|
||||
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro,
|
||||
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedProFeatureFlag,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{
|
||||
SinkExt, Stream, StreamExt,
|
||||
SinkExt, Stream, StreamExt, TryStreamExt as _,
|
||||
channel::mpsc,
|
||||
future::{BoxFuture, LocalBoxFuture},
|
||||
join,
|
||||
@@ -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
|
||||
@@ -1652,7 +1652,7 @@ impl Render for PromptEditor {
|
||||
|
||||
let error_message = SharedString::from(error.to_string());
|
||||
if error.error_code() == proto::ErrorCode::RateLimitExceeded
|
||||
&& cx.has_flag::<ZedPro>()
|
||||
&& cx.has_flag::<ZedProFeatureFlag>()
|
||||
{
|
||||
el.child(
|
||||
v_flex()
|
||||
@@ -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,
|
||||
)
|
||||
@@ -1966,7 +1966,7 @@ impl PromptEditor {
|
||||
.update(cx, |editor, _| editor.set_read_only(false));
|
||||
}
|
||||
CodegenStatus::Error(error) => {
|
||||
if cx.has_flag::<ZedPro>()
|
||||
if cx.has_flag::<ZedProFeatureFlag>()
|
||||
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
|
||||
&& !dismissed_rate_limit_notice()
|
||||
{
|
||||
@@ -2981,6 +2981,7 @@ impl CodegenAlternative {
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
@@ -3023,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);
|
||||
@@ -3056,7 +3057,8 @@ impl CodegenAlternative {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = StripInvalidSpans::new(stream?.stream);
|
||||
let chunks =
|
||||
StripInvalidSpans::new(stream?.stream.map_err(|e| e.into()));
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
let mut line_diff = LineDiff::default();
|
||||
|
||||
@@ -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::{
|
||||
@@ -294,6 +294,7 @@ impl TerminalInlineAssistant {
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
@@ -748,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(),
|
||||
@@ -755,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 {
|
||||
@@ -459,6 +459,7 @@ pub enum ContextEvent {
|
||||
ShowMaxMonthlySpendReachedError,
|
||||
MessagesEdited,
|
||||
SummaryChanged,
|
||||
SummaryGenerated,
|
||||
StreamedCompletion,
|
||||
StartedThoughtProcess(Range<language::Anchor>),
|
||||
EndedThoughtProcess(language::Anchor),
|
||||
@@ -482,7 +483,7 @@ pub enum ContextEvent {
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct ContextSummary {
|
||||
pub text: String,
|
||||
done: bool,
|
||||
pub done: bool,
|
||||
timestamp: clock::Lamport,
|
||||
}
|
||||
|
||||
@@ -640,14 +641,14 @@ pub struct AssistantContext {
|
||||
contents: Vec<Content>,
|
||||
messages_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: Option<ContextSummary>,
|
||||
pending_summary: Task<Option<()>>,
|
||||
summary_task: Task<Option<()>>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
token_count: Option<usize>,
|
||||
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>,
|
||||
@@ -741,7 +742,7 @@ impl AssistantContext {
|
||||
thought_process_output_sections: Vec::new(),
|
||||
edits_since_last_parse: edits_since_last_slash_command_parse,
|
||||
summary: None,
|
||||
pending_summary: Task::ready(None),
|
||||
summary_task: Task::ready(None),
|
||||
completion_count: Default::default(),
|
||||
pending_completions: Default::default(),
|
||||
token_count: None,
|
||||
@@ -838,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>,
|
||||
@@ -951,7 +952,7 @@ impl AssistantContext {
|
||||
|
||||
fn flush_ops(&mut self, cx: &mut Context<AssistantContext>) {
|
||||
let mut changed_messages = HashSet::default();
|
||||
let mut summary_changed = false;
|
||||
let mut summary_generated = false;
|
||||
|
||||
self.pending_ops.sort_unstable_by_key(|op| op.timestamp());
|
||||
for op in mem::take(&mut self.pending_ops) {
|
||||
@@ -993,7 +994,7 @@ impl AssistantContext {
|
||||
.map_or(true, |summary| new_summary.timestamp > summary.timestamp)
|
||||
{
|
||||
self.summary = Some(new_summary);
|
||||
summary_changed = true;
|
||||
summary_generated = true;
|
||||
}
|
||||
}
|
||||
ContextOperation::SlashCommandStarted {
|
||||
@@ -1072,8 +1073,9 @@ impl AssistantContext {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
if summary_changed {
|
||||
if summary_generated {
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
cx.emit(ContextEvent::SummaryGenerated);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -1145,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> {
|
||||
@@ -2557,6 +2559,7 @@ impl AssistantContext {
|
||||
let mut completion_request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
mode: None,
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
@@ -2611,7 +2614,9 @@ impl AssistantContext {
|
||||
.map(MessageContent::Text),
|
||||
);
|
||||
|
||||
completion_request.messages.push(request_message);
|
||||
if !request_message.contents_empty() {
|
||||
completion_request.messages.push(request_message);
|
||||
}
|
||||
}
|
||||
|
||||
if let RequestType::SuggestEdits = request_type {
|
||||
@@ -2945,7 +2950,7 @@ impl AssistantContext {
|
||||
self.message_anchors.insert(insertion_ix, new_anchor);
|
||||
}
|
||||
|
||||
pub fn summarize(&mut self, replace_old: bool, cx: &mut Context<Self>) {
|
||||
pub fn summarize(&mut self, mut replace_old: bool, cx: &mut Context<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
return;
|
||||
};
|
||||
@@ -2965,7 +2970,18 @@ impl AssistantContext {
|
||||
cache: false,
|
||||
});
|
||||
|
||||
self.pending_summary = cx.spawn(async move |this, cx| {
|
||||
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can
|
||||
// be displayed.
|
||||
if self.summary.is_none() {
|
||||
self.summary = Some(ContextSummary {
|
||||
text: "".to_string(),
|
||||
done: false,
|
||||
timestamp: clock::Lamport::default(),
|
||||
});
|
||||
replace_old = true;
|
||||
}
|
||||
|
||||
self.summary_task = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
let stream = model.model.stream_completion_text(request, &cx);
|
||||
let mut messages = stream.await?;
|
||||
@@ -2990,6 +3006,7 @@ impl AssistantContext {
|
||||
};
|
||||
this.push_op(operation, cx);
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
cx.emit(ContextEvent::SummaryGenerated);
|
||||
})?;
|
||||
|
||||
// Stop if the LLM generated multiple lines.
|
||||
@@ -3010,6 +3027,7 @@ impl AssistantContext {
|
||||
};
|
||||
this.push_op(operation, cx);
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
cx.emit(ContextEvent::SummaryGenerated);
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -3163,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 {
|
||||
@@ -3175,14 +3193,14 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
this.update(cx, |this, _| this.path = Some(new_path))?;
|
||||
this.update(cx, |this, _| this.path = Some(new_path.into()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
|
||||
pub fn set_custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
|
||||
let timestamp = self.next_timestamp();
|
||||
let summary = self.summary.get_or_insert(ContextSummary::default());
|
||||
summary.timestamp = timestamp;
|
||||
@@ -3190,6 +3208,15 @@ impl AssistantContext {
|
||||
summary.text = custom_summary;
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
}
|
||||
|
||||
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Text Thread");
|
||||
|
||||
pub fn summary_or_default(&self) -> SharedString {
|
||||
self.summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.text.clone().into())
|
||||
.unwrap_or(Self::DEFAULT_SUMMARY)
|
||||
}
|
||||
}
|
||||
|
||||
fn trimmed_text_in_range(buffer: &BufferSnapshot, range: Range<text::Anchor>) -> String {
|
||||
@@ -3562,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, borrow::Cow, 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,
|
||||
)
|
||||
@@ -618,6 +625,7 @@ impl ContextEditor {
|
||||
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
|
||||
});
|
||||
}
|
||||
ContextEvent::SummaryGenerated => {}
|
||||
ContextEvent::StartedThoughtProcess(range) => {
|
||||
let creases = self.insert_thought_process_output_sections(
|
||||
[(
|
||||
@@ -2179,13 +2187,8 @@ impl ContextEditor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &App) -> Cow<str> {
|
||||
self.context
|
||||
.read(cx)
|
||||
.summary()
|
||||
.map(|summary| summary.text.clone())
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
|
||||
pub fn title(&self, cx: &App) -> SharedString {
|
||||
self.context.read(cx).summary_or_default()
|
||||
}
|
||||
|
||||
fn render_patch_block(
|
||||
@@ -3160,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)) {
|
||||
@@ -3768,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() {
|
||||
|
||||
@@ -24,10 +24,12 @@ language.workspace = true
|
||||
language_model.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod action_log;
|
||||
pub mod outline;
|
||||
mod tool_registry;
|
||||
mod tool_schema;
|
||||
mod tool_working_set;
|
||||
@@ -10,14 +11,16 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::AnyElement;
|
||||
use gpui::AnyWindowHandle;
|
||||
use gpui::Context;
|
||||
use gpui::IntoElement;
|
||||
use gpui::Window;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use gpui::{App, Entity, SharedString, Task, WeakEntity};
|
||||
use icons::IconName;
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use project::Project;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::action_log::*;
|
||||
pub use crate::tool_registry::*;
|
||||
@@ -49,6 +52,13 @@ impl ToolUseStatus {
|
||||
ToolUseStatus::Error(out) => out.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<SharedString> {
|
||||
match self {
|
||||
ToolUseStatus::Error(out) => Some(out.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of running a tool, containing both the asynchronous output
|
||||
@@ -65,6 +75,7 @@ pub trait ToolCard: 'static + Sized {
|
||||
&mut self,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement;
|
||||
}
|
||||
@@ -76,6 +87,7 @@ pub struct AnyToolCard {
|
||||
entity: gpui::AnyEntity,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> AnyElement,
|
||||
}
|
||||
@@ -86,11 +98,14 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
|
||||
entity: gpui::AnyEntity,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let entity = entity.downcast::<T>().unwrap();
|
||||
entity.update(cx, |entity, cx| {
|
||||
entity.render(status, window, cx).into_any_element()
|
||||
entity
|
||||
.render(status, window, workspace, cx)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -102,8 +117,14 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
|
||||
}
|
||||
|
||||
impl AnyToolCard {
|
||||
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
(self.render)(self.entity.clone(), status, window, cx)
|
||||
pub fn render(
|
||||
&self,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
(self.render)(self.entity.clone(), status, window, workspace, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +184,7 @@ pub trait Tool: 'static + Send + Sync {
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult;
|
||||
}
|
||||
|
||||
132
crates/assistant_tool/src/outline.rs
Normal file
132
crates/assistant_tool/src/outline.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use crate::ActionLog;
|
||||
use anyhow::{Result, anyhow};
|
||||
use gpui::{AsyncApp, Entity};
|
||||
use language::{OutlineItem, ParseStatus};
|
||||
use project::Project;
|
||||
use regex::Regex;
|
||||
use std::fmt::Write;
|
||||
use text::Point;
|
||||
|
||||
/// For files over this size, instead of reading them (or including them in context),
|
||||
/// we automatically provide the file's symbol outline instead, with line numbers.
|
||||
pub const AUTO_OUTLINE_SIZE: usize = 16384;
|
||||
|
||||
pub async fn file_outline(
|
||||
project: Entity<Project>,
|
||||
path: String,
|
||||
action_log: Entity<ActionLog>,
|
||||
regex: Option<Regex>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<String> {
|
||||
let buffer = {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&path, cx)
|
||||
.ok_or_else(|| anyhow!("Path {path} not found in project"))
|
||||
})??;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.track_buffer(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
// Wait until the buffer has been fully parsed, so that we can read its outline.
|
||||
let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
|
||||
while *parse_status.borrow() != ParseStatus::Idle {
|
||||
parse_status.changed().await?;
|
||||
}
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
||||
let Some(outline) = snapshot.outline(None) else {
|
||||
return Err(anyhow!("No outline information available for this file."));
|
||||
};
|
||||
|
||||
render_outline(
|
||||
outline
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| item.to_point(&snapshot)),
|
||||
regex,
|
||||
0,
|
||||
usize::MAX,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn render_outline(
|
||||
items: impl IntoIterator<Item = OutlineItem<Point>>,
|
||||
regex: Option<Regex>,
|
||||
offset: usize,
|
||||
results_per_page: usize,
|
||||
) -> Result<String> {
|
||||
let mut items = items.into_iter().skip(offset);
|
||||
|
||||
let entries = items
|
||||
.by_ref()
|
||||
.filter(|item| {
|
||||
regex
|
||||
.as_ref()
|
||||
.is_none_or(|regex| regex.is_match(&item.text))
|
||||
})
|
||||
.take(results_per_page)
|
||||
.collect::<Vec<_>>();
|
||||
let has_more = items.next().is_some();
|
||||
|
||||
let mut output = String::new();
|
||||
let entries_rendered = render_entries(&mut output, entries);
|
||||
|
||||
// Calculate pagination information
|
||||
let page_start = offset + 1;
|
||||
let page_end = offset + entries_rendered;
|
||||
let total_symbols = if has_more {
|
||||
format!("more than {}", page_end)
|
||||
} else {
|
||||
page_end.to_string()
|
||||
};
|
||||
|
||||
// Add pagination information
|
||||
if has_more {
|
||||
writeln!(&mut output, "\nShowing symbols {page_start}-{page_end} (there were more symbols found; use offset: {page_end} to see next page)",
|
||||
)
|
||||
} else {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\nShowing symbols {page_start}-{page_end} (total symbols: {total_symbols})",
|
||||
)
|
||||
}
|
||||
.ok();
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn render_entries(
|
||||
output: &mut String,
|
||||
items: impl IntoIterator<Item = OutlineItem<Point>>,
|
||||
) -> usize {
|
||||
let mut entries_rendered = 0;
|
||||
|
||||
for item in items {
|
||||
// Indent based on depth ("" for level 0, " " for level 1, etc.)
|
||||
for _ in 0..item.depth {
|
||||
output.push(' ');
|
||||
}
|
||||
output.push_str(&item.text);
|
||||
|
||||
// Add position information - convert to 1-based line numbers for display
|
||||
let start_line = item.range.start.row + 1;
|
||||
let end_line = item.range.end.row + 1;
|
||||
|
||||
if start_line == end_line {
|
||||
writeln!(output, " [L{}]", start_line).ok();
|
||||
} else {
|
||||
writeln!(output, " [L{}-{}]", start_line, end_line).ok();
|
||||
}
|
||||
entries_rendered += 1;
|
||||
}
|
||||
|
||||
entries_rendered
|
||||
}
|
||||
@@ -10,6 +10,11 @@ pub fn adapt_schema_to_format(
|
||||
json: &mut Value,
|
||||
format: LanguageModelToolSchemaFormat,
|
||||
) -> Result<()> {
|
||||
if let Value::Object(obj) = json {
|
||||
obj.remove("$schema");
|
||||
obj.remove("title");
|
||||
}
|
||||
|
||||
match format {
|
||||
LanguageModelToolSchemaFormat::JsonSchema => Ok(()),
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => adapt_to_json_schema_subset(json),
|
||||
@@ -30,7 +35,12 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
const KEYS_TO_REMOVE: [&str; 2] = ["format", "$schema"];
|
||||
const KEYS_TO_REMOVE: [&str; 4] = [
|
||||
"format",
|
||||
"additionalProperties",
|
||||
"exclusiveMinimum",
|
||||
"exclusiveMaximum",
|
||||
];
|
||||
for key in KEYS_TO_REMOVE {
|
||||
obj.remove(key);
|
||||
}
|
||||
@@ -45,7 +55,7 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
|
||||
}
|
||||
|
||||
// If a type is not specified for an input parameter, add a default type
|
||||
if obj.contains_key("description")
|
||||
if matches!(obj.get("description"), Some(Value::String(_)))
|
||||
&& !obj.contains_key("type")
|
||||
&& !(obj.contains_key("anyOf")
|
||||
|| obj.contains_key("oneOf")
|
||||
@@ -117,14 +127,37 @@ mod tests {
|
||||
"type": "string"
|
||||
})
|
||||
);
|
||||
|
||||
// Ensure that we do not add a type if it is an object
|
||||
let mut json = json!({
|
||||
"description": {
|
||||
"value": "abc",
|
||||
"type": "string"
|
||||
}
|
||||
});
|
||||
|
||||
adapt_to_json_schema_subset(&mut json).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
json,
|
||||
json!({
|
||||
"description": {
|
||||
"value": "abc",
|
||||
"type": "string"
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_removes_format() {
|
||||
fn test_transform_removes_unsupported_keys() {
|
||||
let mut json = json!({
|
||||
"description": "A test field",
|
||||
"type": "integer",
|
||||
"format": "uint32"
|
||||
"format": "uint32",
|
||||
"exclusiveMinimum": 0,
|
||||
"exclusiveMaximum": 100,
|
||||
"additionalProperties": false
|
||||
});
|
||||
|
||||
adapt_to_json_schema_subset(&mut json).unwrap();
|
||||
|
||||
@@ -14,10 +14,11 @@ path = "src/assistant_tools.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
feature_flags.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
@@ -33,14 +34,19 @@ regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
task.workspace = true
|
||||
terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
web_search.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
worktree.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
@@ -48,6 +54,7 @@ project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
task = { workspace = true, features = ["test-support"]}
|
||||
tree-sitter-rust.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -9,12 +9,12 @@ mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
mod edit_file_tool;
|
||||
mod fetch_tool;
|
||||
mod find_path_tool;
|
||||
mod grep_tool;
|
||||
mod list_directory_tool;
|
||||
mod move_path_tool;
|
||||
mod now_tool;
|
||||
mod open_tool;
|
||||
mod path_search_tool;
|
||||
mod read_file_tool;
|
||||
mod rename_tool;
|
||||
mod replace;
|
||||
@@ -29,9 +29,9 @@ use std::sync::Arc;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use copy_path_tool::CopyPathTool;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::App;
|
||||
use http_client::HttpClientWithUrl;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use move_path_tool::MovePathTool;
|
||||
use web_search_tool::WebSearchTool;
|
||||
|
||||
@@ -45,18 +45,21 @@ use crate::delete_path_tool::DeletePathTool;
|
||||
use crate::diagnostics_tool::DiagnosticsTool;
|
||||
use crate::edit_file_tool::EditFileTool;
|
||||
use crate::fetch_tool::FetchTool;
|
||||
use crate::find_path_tool::FindPathTool;
|
||||
use crate::grep_tool::GrepTool;
|
||||
use crate::list_directory_tool::ListDirectoryTool;
|
||||
use crate::now_tool::NowTool;
|
||||
use crate::open_tool::OpenTool;
|
||||
use crate::path_search_tool::PathSearchTool;
|
||||
use crate::read_file_tool::ReadFileTool;
|
||||
use crate::rename_tool::RenameTool;
|
||||
use crate::symbol_info_tool::SymbolInfoTool;
|
||||
use crate::terminal_tool::TerminalTool;
|
||||
use crate::thinking_tool::ThinkingTool;
|
||||
|
||||
pub use path_search_tool::PathSearchToolInput;
|
||||
pub use create_file_tool::CreateFileToolInput;
|
||||
pub use edit_file_tool::EditFileToolInput;
|
||||
pub use find_path_tool::FindPathToolInput;
|
||||
pub use read_file_tool::ReadFileToolInput;
|
||||
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
@@ -78,41 +81,79 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(OpenTool);
|
||||
registry.register_tool(CodeSymbolsTool);
|
||||
registry.register_tool(ContentsTool);
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(FindPathTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(GrepTool);
|
||||
registry.register_tool(RenameTool);
|
||||
registry.register_tool(ThinkingTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
|
||||
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
|
||||
move |is_enabled, cx| {
|
||||
if is_enabled {
|
||||
ToolRegistry::global(cx).register_tool(WebSearchTool);
|
||||
} else {
|
||||
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
|
||||
cx.subscribe(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
move |registry, event, cx| match event {
|
||||
language_model::Event::DefaultModelChanged => {
|
||||
let using_zed_provider = registry
|
||||
.read(cx)
|
||||
.default_model()
|
||||
.map_or(false, |default| default.is_provided_by_zed());
|
||||
if using_zed_provider {
|
||||
ToolRegistry::global(cx).register_tool(WebSearchTool);
|
||||
} else {
|
||||
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use http_client::FakeHttpClient;
|
||||
|
||||
use super::*;
|
||||
use client::Client;
|
||||
use clock::FakeSystemClock;
|
||||
use http_client::FakeHttpClient;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
#[test]
|
||||
fn test_json_schema() {
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
struct GetWeatherTool {
|
||||
location: String,
|
||||
}
|
||||
|
||||
let schema = schema::json_schema_for::<GetWeatherTool>(
|
||||
language_model::LanguageModelToolSchemaFormat::JsonSchema,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
schema,
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["location"],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_builtin_tool_schema_compatibility(cx: &mut App) {
|
||||
crate::init(
|
||||
Arc::new(http_client::HttpClientWithUrl::new(
|
||||
FakeHttpClient::with_200_response(),
|
||||
"https://zed.dev",
|
||||
None,
|
||||
)),
|
||||
settings::init(cx);
|
||||
|
||||
let client = Client::new(
|
||||
Arc::new(FakeSystemClock::new()),
|
||||
FakeHttpClient::with_200_response(),
|
||||
cx,
|
||||
);
|
||||
language_model::init(client.clone(), cx);
|
||||
crate::init(client.http_client(), cx);
|
||||
|
||||
for tool in ToolRegistry::global(cx).tools() {
|
||||
let actual_schema = tool
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
|
||||
use futures::future::join_all;
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -97,7 +97,7 @@ pub struct BatchToolInput {
|
||||
/// }
|
||||
/// },
|
||||
/// {
|
||||
/// "name": "path_search",
|
||||
/// "name": "find_path",
|
||||
/// "input": {
|
||||
/// "glob": "**/*test*.rs"
|
||||
/// }
|
||||
@@ -218,6 +218,7 @@ impl Tool for BatchTool {
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<BatchToolInput>(input) {
|
||||
@@ -258,7 +259,9 @@ impl Tool for BatchTool {
|
||||
let action_log = action_log.clone();
|
||||
let messages = messages.clone();
|
||||
let tool_result = cx
|
||||
.update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
|
||||
.update(|cx| {
|
||||
tool.run(invocation.input, &messages, project, action_log, window, cx)
|
||||
})
|
||||
.map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
|
||||
|
||||
tasks.push(tool_result.output);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language::{self, Anchor, Buffer, ToPointUtf16};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{self, LspAction, Project};
|
||||
@@ -140,6 +140,7 @@ impl Tool for CodeActionTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CodeActionToolInput>(input) {
|
||||
|
||||
@@ -4,17 +4,17 @@ use std::sync::Arc;
|
||||
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::outline;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, AsyncApp, Entity, Task};
|
||||
use language::{OutlineItem, ParseStatus, Point};
|
||||
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{Project, Symbol};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CodeSymbolsInput {
|
||||
@@ -102,7 +102,7 @@ impl Tool for CodeSymbolsTool {
|
||||
|
||||
match &input.path {
|
||||
Some(path) => {
|
||||
let path = MarkdownString::inline_code(path);
|
||||
let path = MarkdownInlineCode(path);
|
||||
if page > 1 {
|
||||
format!("List page {page} of code symbols for {path}")
|
||||
} else {
|
||||
@@ -128,6 +128,7 @@ impl Tool for CodeSymbolsTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
|
||||
@@ -147,59 +148,13 @@ impl Tool for CodeSymbolsTool {
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| match input.path {
|
||||
Some(path) => file_outline(project, path, action_log, regex, cx).await,
|
||||
Some(path) => outline::file_outline(project, path, action_log, regex, cx).await,
|
||||
None => project_symbols(project, regex, input.offset, cx).await,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn file_outline(
|
||||
project: Entity<Project>,
|
||||
path: String,
|
||||
action_log: Entity<ActionLog>,
|
||||
regex: Option<Regex>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<String> {
|
||||
let buffer = {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&path, cx)
|
||||
.ok_or_else(|| anyhow!("Path {path} not found in project"))
|
||||
})??;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?
|
||||
};
|
||||
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.track_buffer(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
// Wait until the buffer has been fully parsed, so that we can read its outline.
|
||||
let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
|
||||
while *parse_status.borrow() != ParseStatus::Idle {
|
||||
parse_status.changed().await?;
|
||||
}
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
||||
let Some(outline) = snapshot.outline(None) else {
|
||||
return Err(anyhow!("No outline information available for this file."));
|
||||
};
|
||||
|
||||
render_outline(
|
||||
outline
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| item.to_point(&snapshot)),
|
||||
regex,
|
||||
0,
|
||||
usize::MAX,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn project_symbols(
|
||||
project: Entity<Project>,
|
||||
regex: Option<Regex>,
|
||||
@@ -290,77 +245,3 @@ async fn project_symbols(
|
||||
output
|
||||
})
|
||||
}
|
||||
|
||||
async fn render_outline(
|
||||
items: impl IntoIterator<Item = OutlineItem<Point>>,
|
||||
regex: Option<Regex>,
|
||||
offset: usize,
|
||||
results_per_page: usize,
|
||||
) -> Result<String> {
|
||||
let mut items = items.into_iter().skip(offset);
|
||||
|
||||
let entries = items
|
||||
.by_ref()
|
||||
.filter(|item| {
|
||||
regex
|
||||
.as_ref()
|
||||
.is_none_or(|regex| regex.is_match(&item.text))
|
||||
})
|
||||
.take(results_per_page)
|
||||
.collect::<Vec<_>>();
|
||||
let has_more = items.next().is_some();
|
||||
|
||||
let mut output = String::new();
|
||||
let entries_rendered = render_entries(&mut output, entries);
|
||||
|
||||
// Calculate pagination information
|
||||
let page_start = offset + 1;
|
||||
let page_end = offset + entries_rendered;
|
||||
let total_symbols = if has_more {
|
||||
format!("more than {}", page_end)
|
||||
} else {
|
||||
page_end.to_string()
|
||||
};
|
||||
|
||||
// Add pagination information
|
||||
if has_more {
|
||||
writeln!(&mut output, "\nShowing symbols {page_start}-{page_end} (there were more symbols found; use offset: {page_end} to see next page)",
|
||||
)
|
||||
} else {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\nShowing symbols {page_start}-{page_end} (total symbols: {total_symbols})",
|
||||
)
|
||||
}
|
||||
.ok();
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn render_entries(
|
||||
output: &mut String,
|
||||
items: impl IntoIterator<Item = OutlineItem<Point>>,
|
||||
) -> usize {
|
||||
let mut entries_rendered = 0;
|
||||
|
||||
for item in items {
|
||||
// Indent based on depth ("" for level 0, " " for level 1, etc.)
|
||||
for _ in 0..item.depth {
|
||||
output.push(' ');
|
||||
}
|
||||
output.push_str(&item.text);
|
||||
|
||||
// Add position information - convert to 1-based line numbers for display
|
||||
let start_line = item.range.start.row + 1;
|
||||
let end_line = item.range.end.row + 1;
|
||||
|
||||
if start_line == end_line {
|
||||
writeln!(output, " [L{}]", start_line).ok();
|
||||
} else {
|
||||
writeln!(output, " [L{}-{}]", start_line, end_line).ok();
|
||||
}
|
||||
entries_rendered += 1;
|
||||
}
|
||||
|
||||
entries_rendered
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult, outline};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use itertools::Itertools;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -11,13 +11,9 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write, path::Path};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
/// If the model requests to read a file whose size exceeds this, then
|
||||
/// the tool will return the file's symbol outline instead of its contents,
|
||||
/// and suggest trying again using line ranges from the outline.
|
||||
const MAX_FILE_SIZE_TO_READ: usize = 16384;
|
||||
|
||||
/// If the model requests to list the entries in a directory with more
|
||||
/// entries than this, then the tool will return a subset of the entries
|
||||
/// and suggest trying again.
|
||||
@@ -82,7 +78,7 @@ impl Tool for ContentsTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<ContentsToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let path = MarkdownString::inline_code(&input.path);
|
||||
let path = MarkdownInlineCode(&input.path);
|
||||
|
||||
match (input.start, input.end) {
|
||||
(Some(start), None) => format!("Read {path} (from line {start})"),
|
||||
@@ -102,6 +98,7 @@ impl Tool for ContentsTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<ContentsToolInput>(input) {
|
||||
@@ -217,7 +214,7 @@ impl Tool for ContentsTool {
|
||||
// No line ranges specified, so check file size to see if it's too big.
|
||||
let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
|
||||
|
||||
if file_size <= MAX_FILE_SIZE_TO_READ {
|
||||
if file_size <= outline::AUTO_OUTLINE_SIZE {
|
||||
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
@@ -228,7 +225,7 @@ impl Tool for ContentsTool {
|
||||
} else {
|
||||
// File is too big, so return its outline and a suggestion to
|
||||
// read again with a line number range specified.
|
||||
let outline = file_outline(project, file_path, action_log, None, cx).await?;
|
||||
let outline = outline::file_outline(project, file_path, action_log, None, cx).await?;
|
||||
|
||||
Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start and end fields to see the implementations of symbols in the outline."))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::AnyWindowHandle;
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
@@ -9,7 +10,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CopyPathToolInput {
|
||||
@@ -62,8 +63,8 @@ impl Tool for CopyPathTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CopyPathToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let src = MarkdownString::inline_code(&input.source_path);
|
||||
let dest = MarkdownString::inline_code(&input.destination_path);
|
||||
let src = MarkdownInlineCode(&input.source_path);
|
||||
let dest = MarkdownInlineCode(&input.destination_path);
|
||||
format!("Copy {src} to {dest}")
|
||||
}
|
||||
Err(_) => "Copy path".to_string(),
|
||||
@@ -76,6 +77,7 @@ impl Tool for CopyPathTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CopyPathToolInput>(input) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::AnyWindowHandle;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
@@ -9,7 +10,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateDirectoryToolInput {
|
||||
@@ -52,10 +53,7 @@ impl Tool for CreateDirectoryTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CreateDirectoryToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
format!(
|
||||
"Create directory {}",
|
||||
MarkdownString::inline_code(&input.path)
|
||||
)
|
||||
format!("Create directory {}", MarkdownInlineCode(&input.path))
|
||||
}
|
||||
Err(_) => "Create directory".to_string(),
|
||||
}
|
||||
@@ -67,6 +65,7 @@ impl Tool for CreateDirectoryTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::AnyWindowHandle;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
@@ -9,7 +10,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateFileToolInput {
|
||||
@@ -23,6 +24,9 @@ pub struct CreateFileToolInput {
|
||||
///
|
||||
/// You can create a new file by providing a path of "directory1/new_file.txt"
|
||||
/// </example>
|
||||
///
|
||||
/// Make sure to include this field before the `contents` field in the input object
|
||||
/// so that we can display it immediately.
|
||||
pub path: String,
|
||||
|
||||
/// The text contents of the file to create.
|
||||
@@ -69,7 +73,7 @@ impl Tool for CreateFileTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let path = MarkdownString::inline_code(&input.path);
|
||||
let path = MarkdownInlineCode(&input.path);
|
||||
format!("Create file {path}")
|
||||
}
|
||||
Err(_) => DEFAULT_UI_TEXT.to_string(),
|
||||
@@ -89,6 +93,7 @@ impl Tool for CreateFileTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
@@ -62,6 +62,7 @@ impl Tool for DeletePathTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language::{DiagnosticSeverity, OffsetRangeExt};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -9,7 +9,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DiagnosticsToolInput {
|
||||
@@ -66,11 +66,11 @@ impl Tool for DiagnosticsTool {
|
||||
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
|
||||
.ok()
|
||||
.and_then(|input| match input.path {
|
||||
Some(path) if !path.is_empty() => Some(MarkdownString::inline_code(&path)),
|
||||
Some(path) if !path.is_empty() => Some(path),
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
format!("Check diagnostics for {path}")
|
||||
format!("Check diagnostics for {}", MarkdownInlineCode(&path))
|
||||
} else {
|
||||
"Check project diagnostics".to_string()
|
||||
}
|
||||
@@ -82,6 +82,7 @@ impl Tool for DiagnosticsTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
match serde_json::from_value::<DiagnosticsToolInput>(input)
|
||||
|
||||
@@ -1,18 +1,43 @@
|
||||
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
|
||||
use crate::{
|
||||
replace::{replace_exact, replace_with_flexible_indent},
|
||||
schema::json_schema_for,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId, Task, WeakEntity,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
|
||||
language_settings::SoftWrap,
|
||||
};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use ui::IconName;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::{Disclosure, Tooltip, Window, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::replace::replace_exact;
|
||||
pub struct EditFileTool;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditFileToolInput {
|
||||
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year in `page_footer`</example>
|
||||
///
|
||||
/// Make sure to include this field before all the others in the input object
|
||||
/// so that we can display it immediately.
|
||||
pub display_description: String,
|
||||
|
||||
/// The full path of the file to modify in the project.
|
||||
///
|
||||
/// WARNING: When specifying which file path need changing, you MUST
|
||||
@@ -34,12 +59,6 @@ pub struct EditFileToolInput {
|
||||
/// </example>
|
||||
pub path: PathBuf,
|
||||
|
||||
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year in `page_footer`</example>
|
||||
pub display_description: String,
|
||||
|
||||
/// The text to replace.
|
||||
pub old_string: String,
|
||||
|
||||
@@ -59,8 +78,6 @@ struct PartialInput {
|
||||
new_string: String,
|
||||
}
|
||||
|
||||
pub struct EditFileTool;
|
||||
|
||||
const DEFAULT_UI_TEXT: &str = "Editing file";
|
||||
|
||||
impl Tool for EditFileTool {
|
||||
@@ -113,6 +130,7 @@ impl Tool for EditFileTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<EditFileToolInput>(input) {
|
||||
@@ -120,7 +138,18 @@ impl Tool for EditFileTool {
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let card = window.and_then(|window| {
|
||||
window
|
||||
.update(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
let card_clone = card.clone();
|
||||
let task = cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&input.path, cx)
|
||||
@@ -128,26 +157,38 @@ impl Tool for EditFileTool {
|
||||
})??;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
if input.old_string.is_empty() {
|
||||
return Err(anyhow!("`old_string` cannot be empty. Use a different tool if you want to create a file."));
|
||||
return Err(anyhow!(
|
||||
"`old_string` can't be empty, use another tool if you want to create a file."
|
||||
));
|
||||
}
|
||||
|
||||
if input.old_string == input.new_string {
|
||||
return Err(anyhow!("The `old_string` and `new_string` are identical, so no changes would be made."));
|
||||
return Err(anyhow!(
|
||||
"The `old_string` and `new_string` are identical, so no changes would be made."
|
||||
));
|
||||
}
|
||||
|
||||
let result = cx
|
||||
.background_spawn(async move {
|
||||
// Try to match exactly
|
||||
let diff = replace_exact(&input.old_string, &input.new_string, &snapshot)
|
||||
.await
|
||||
// If that fails, try being flexible about indentation
|
||||
.or_else(|| replace_with_flexible_indent(&input.old_string, &input.new_string, &snapshot))?;
|
||||
.await
|
||||
// If that fails, try being flexible about indentation
|
||||
.or_else(|| {
|
||||
replace_with_flexible_indent(
|
||||
&input.old_string,
|
||||
&input.new_string,
|
||||
&snapshot,
|
||||
)
|
||||
})?;
|
||||
|
||||
if diff.edits.is_empty() {
|
||||
return None;
|
||||
@@ -177,41 +218,476 @@ impl Tool for EditFileTool {
|
||||
}
|
||||
})?;
|
||||
|
||||
return Err(err)
|
||||
return Err(err);
|
||||
};
|
||||
|
||||
let snapshot = cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.track_buffer(buffer.clone(), cx)
|
||||
});
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
|
||||
let snapshot = buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.apply_diff(diff, cx);
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.snapshot()
|
||||
});
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), cx)
|
||||
});
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
snapshot
|
||||
})?;
|
||||
|
||||
project.update( cx, |project, cx| {
|
||||
project.save_buffer(buffer, cx)
|
||||
})?.await?;
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||
.await?;
|
||||
|
||||
let diff_str = cx.background_spawn(async move {
|
||||
let new_text = snapshot.text();
|
||||
language::unified_diff(&old_text, &new_text)
|
||||
}).await;
|
||||
let new_text = snapshot.text();
|
||||
let diff_str = cx
|
||||
.background_spawn({
|
||||
let old_text = old_text.clone();
|
||||
let new_text = new_text.clone();
|
||||
async move { language::unified_diff(&old_text, &new_text) }
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(card) = card_clone {
|
||||
card.update(cx, |card, cx| {
|
||||
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
|
||||
Ok(format!(
|
||||
"Edited {}:\n\n```diff\n{}\n```",
|
||||
input.path.display(),
|
||||
diff_str
|
||||
))
|
||||
});
|
||||
|
||||
}).into()
|
||||
ToolResult {
|
||||
output: task,
|
||||
card: card.map(AnyToolCard::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EditFileToolCard {
|
||||
path: PathBuf,
|
||||
editor: Entity<Editor>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
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,
|
||||
}
|
||||
|
||||
impl EditFileToolCard {
|
||||
fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
|
||||
let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::new(
|
||||
EditorMode::Full {
|
||||
scale_ui_elements_with_buffer_font_size: false,
|
||||
show_active_line_background: false,
|
||||
sized_by_content: true,
|
||||
},
|
||||
multibuffer.clone(),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
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);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor
|
||||
});
|
||||
Self {
|
||||
editor_unique_id: editor.entity_id(),
|
||||
path,
|
||||
project,
|
||||
editor,
|
||||
multibuffer,
|
||||
diff_task: None,
|
||||
preview_expanded: true,
|
||||
error_expanded: false,
|
||||
full_height_expanded: false,
|
||||
total_lines: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_diff(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
old_text: String,
|
||||
new_text: String,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
self.diff_task = Some(cx.spawn(async move |this, cx| {
|
||||
let buffer = build_buffer(new_text, path.clone(), &language_registry, cx).await?;
|
||||
let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
|
||||
|
||||
this.update(cx, |this, 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
|
||||
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
let (_, is_newly_added) = multibuffer.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer,
|
||||
diff_hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
);
|
||||
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();
|
||||
})
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolCard for EditFileToolCard {
|
||||
fn render(
|
||||
&mut self,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
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))
|
||||
.w_full()
|
||||
.max_w_full()
|
||||
.px_1()
|
||||
.gap_0p5()
|
||||
.cursor_pointer()
|
||||
.rounded_sm()
|
||||
.opacity(0.8)
|
||||
.hover(|label| {
|
||||
label
|
||||
.opacity(1.)
|
||||
.bg(cx.theme().colors().element_hover.opacity(0.5))
|
||||
})
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
Icon::new(IconName::Pencil)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(rems(0.8125))
|
||||
.child(self.path.display().to_string())
|
||||
.ml_1p5()
|
||||
.mr_0p5(),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Ignored),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let path = self.path.clone();
|
||||
let workspace = workspace.clone();
|
||||
move |_, window, cx| {
|
||||
workspace
|
||||
.update(cx, {
|
||||
|workspace, cx| {
|
||||
let Some(project_path) =
|
||||
workspace.project().read(cx).find_project_path(&path, cx)
|
||||
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(
|
||||
language::Point::new(0, 0),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.into_any_element();
|
||||
|
||||
let codeblock_header_bg = cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_background
|
||||
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
|
||||
|
||||
let codeblock_header = h_flex()
|
||||
.flex_none()
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.rounded_t_md()
|
||||
.when(!failed, |header| header.bg(codeblock_header_bg))
|
||||
.child(path_label_button)
|
||||
.map(|container| {
|
||||
if failed {
|
||||
container.child(
|
||||
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(
|
||||
Disclosure::new(
|
||||
("edit-file-disclosure", self.editor_unique_id),
|
||||
self.preview_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener(
|
||||
move |this, _event, _window, _cx| {
|
||||
this.preview_expanded = !this.preview_expanded;
|
||||
},
|
||||
)),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
(IconName::ChevronUp, "Collapse Code Block")
|
||||
} else {
|
||||
(IconName::ChevronDown, "Expand Code Block")
|
||||
};
|
||||
|
||||
let gradient_overlay = div()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h_2_5()
|
||||
.rounded_b_lg()
|
||||
.bg(gpui::linear_gradient(
|
||||
0.,
|
||||
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
|
||||
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
|
||||
));
|
||||
|
||||
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()
|
||||
.when(failed, |card| card.border_dashed())
|
||||
.border_color(border_color)
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.child(codeblock_header)
|
||||
.when(failed && self.error_expanded, |card| {
|
||||
card.child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
.gap_1()
|
||||
.border_t_1()
|
||||
.border_dashed()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.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(
|
||||
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)
|
||||
.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;
|
||||
})),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_buffer(
|
||||
mut text: String,
|
||||
path: Arc<Path>,
|
||||
language_registry: &Arc<language::LanguageRegistry>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<Buffer>> {
|
||||
let line_ending = LineEnding::detect(&text);
|
||||
LineEnding::normalize(&mut text);
|
||||
let text = Rope::from(text);
|
||||
let language = cx
|
||||
.update(|_cx| language_registry.language_for_file_path(&path))?
|
||||
.await
|
||||
.ok();
|
||||
let buffer = cx.new(|cx| {
|
||||
let buffer = TextBuffer::new_normalized(
|
||||
0,
|
||||
cx.entity_id().as_non_zero_u64().into(),
|
||||
line_ending,
|
||||
text,
|
||||
);
|
||||
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
|
||||
buffer.set_language(language, cx);
|
||||
buffer
|
||||
})?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
async fn build_buffer_diff(
|
||||
mut old_text: String,
|
||||
buffer: &Entity<Buffer>,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<BufferDiff>> {
|
||||
LineEnding::normalize(&mut old_text);
|
||||
|
||||
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
|
||||
|
||||
let base_buffer = cx
|
||||
.update(|cx| {
|
||||
Buffer::build_snapshot(
|
||||
old_text.clone().into(),
|
||||
buffer.language().cloned(),
|
||||
Some(language_registry.clone()),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
let diff_snapshot = cx
|
||||
.update(|cx| {
|
||||
BufferDiffSnapshot::new_with_base_buffer(
|
||||
buffer.text.clone(),
|
||||
Some(old_text.into()),
|
||||
base_buffer,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer.text, cx);
|
||||
diff.set_snapshot(diff_snapshot, &buffer.text, cx);
|
||||
diff
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -219,7 +695,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "",
|
||||
@@ -227,12 +702,11 @@ mod tests {
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), "src/main.rs");
|
||||
assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_description() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "",
|
||||
"display_description": "Fix error handling",
|
||||
@@ -240,12 +714,14 @@ mod tests {
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), "Fix error handling");
|
||||
assert_eq!(
|
||||
EditFileTool.still_streaming_ui_text(&input),
|
||||
"Fix error handling",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path_and_description() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "Fix error handling",
|
||||
@@ -253,12 +729,14 @@ mod tests {
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), "Fix error handling");
|
||||
assert_eq!(
|
||||
EditFileTool.still_streaming_ui_text(&input),
|
||||
"Fix error handling",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_no_path_or_description() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "",
|
||||
"display_description": "",
|
||||
@@ -266,14 +744,19 @@ mod tests {
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
|
||||
assert_eq!(
|
||||
EditFileTool.still_streaming_ui_text(&input),
|
||||
DEFAULT_UI_TEXT,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_null() {
|
||||
let tool = EditFileTool;
|
||||
let input = serde_json::Value::Null;
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
|
||||
assert_eq!(
|
||||
EditFileTool.still_streaming_ui_text(&input),
|
||||
DEFAULT_UI_TEXT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{App, AppContext as _, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task};
|
||||
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
@@ -14,7 +14,7 @@ use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownEscaped;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum ContentType {
|
||||
@@ -134,7 +134,7 @@ impl Tool for FetchTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<FetchToolInput>(input.clone()) {
|
||||
Ok(input) => format!("Fetch {}", MarkdownString::escape(&input.url)),
|
||||
Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)),
|
||||
Err(_) => "Fetch URL".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,7 @@ impl Tool for FetchTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<FetchToolInput>(input) {
|
||||
|
||||
421
crates/assistant_tools/src/find_path_tool.rs
Normal file
421
crates/assistant_tools/src/find_path_tool.rs
Normal file
@@ -0,0 +1,421 @@
|
||||
use crate::{schema::json_schema_for, ui::ToolCallCardHeader};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use editor::Editor;
|
||||
use futures::channel::oneshot::{self, Receiver};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
|
||||
};
|
||||
use language;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
use std::{cmp, path::PathBuf, sync::Arc};
|
||||
use ui::{Disclosure, Tooltip, prelude::*};
|
||||
use util::{ResultExt, paths::PathMatcher};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FindPathToolInput {
|
||||
/// The glob to match against every path in the project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - directory1/a/something.txt
|
||||
/// - directory2/a/things.txt
|
||||
/// - directory3/a/other.txt
|
||||
///
|
||||
/// You can get back the first two paths by providing a glob of "*thing*.txt"
|
||||
/// </example>
|
||||
pub glob: String,
|
||||
|
||||
/// Optional starting position for paginated results (0-based).
|
||||
/// When not provided, starts from the beginning.
|
||||
#[serde(default)]
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
const RESULTS_PER_PAGE: usize = 50;
|
||||
|
||||
pub struct FindPathTool;
|
||||
|
||||
impl Tool for FindPathTool {
|
||||
fn name(&self) -> String {
|
||||
"find_path".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./find_path_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::SearchCode
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<FindPathToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<FindPathToolInput>(input.clone()) {
|
||||
Ok(input) => format!("Find paths matching “`{}`”", input.glob),
|
||||
Err(_) => "Search paths".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let (offset, glob) = match serde_json::from_value::<FindPathToolInput>(input) {
|
||||
Ok(input) => (input.offset, input.glob),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
let card = cx.new(|cx| FindPathToolCard::new(glob.clone(), receiver, cx));
|
||||
|
||||
let search_paths_task = search_paths(&glob, project, cx);
|
||||
|
||||
let task = cx.background_spawn(async move {
|
||||
let matches = search_paths_task.await?;
|
||||
let paginated_matches: &[PathBuf] = &matches[cmp::min(offset, matches.len())
|
||||
..cmp::min(offset + RESULTS_PER_PAGE, matches.len())];
|
||||
|
||||
sender.send(paginated_matches.to_vec()).log_err();
|
||||
|
||||
if matches.is_empty() {
|
||||
Ok("No matches found".to_string())
|
||||
} else {
|
||||
let mut message = format!("Found {} total matches.", matches.len());
|
||||
if matches.len() > RESULTS_PER_PAGE {
|
||||
write!(
|
||||
&mut message,
|
||||
"\nShowing results {}-{} (provide 'offset' parameter for more results):",
|
||||
offset + 1,
|
||||
offset + paginated_matches.len()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
|
||||
write!(&mut message, "\n{}", mat.display()).unwrap();
|
||||
}
|
||||
Ok(message)
|
||||
}
|
||||
});
|
||||
|
||||
ToolResult {
|
||||
output: task,
|
||||
card: Some(card.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
|
||||
let path_matcher = match PathMatcher::new([
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { glob },
|
||||
]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
|
||||
};
|
||||
let snapshots: Vec<_> = project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
Ok(snapshots
|
||||
.iter()
|
||||
.flat_map(|snapshot| {
|
||||
let root_name = PathBuf::from(snapshot.root_name());
|
||||
snapshot
|
||||
.entries(false, 0)
|
||||
.map(move |entry| root_name.join(&entry.path))
|
||||
.filter(|path| path_matcher.is_match(&path))
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
struct FindPathToolCard {
|
||||
paths: Vec<PathBuf>,
|
||||
expanded: bool,
|
||||
glob: String,
|
||||
_receiver_task: Option<Task<Result<()>>>,
|
||||
}
|
||||
|
||||
impl FindPathToolCard {
|
||||
fn new(glob: String, receiver: Receiver<Vec<PathBuf>>, cx: &mut Context<Self>) -> Self {
|
||||
let _receiver_task = cx.spawn(async move |this, cx| {
|
||||
let paths = receiver.await?;
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.paths = paths;
|
||||
})
|
||||
.log_err();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Self {
|
||||
paths: Vec::new(),
|
||||
expanded: false,
|
||||
glob,
|
||||
_receiver_task: Some(_receiver_task),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolCard for FindPathToolCard {
|
||||
fn render(
|
||||
&mut self,
|
||||
_status: &ToolUseStatus,
|
||||
_window: &mut Window,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let matches_label: SharedString = if self.paths.len() == 0 {
|
||||
"No matches".into()
|
||||
} else if self.paths.len() == 1 {
|
||||
"1 match".into()
|
||||
} else {
|
||||
format!("{} matches", self.paths.len()).into()
|
||||
};
|
||||
|
||||
let glob_label = self.glob.to_string();
|
||||
|
||||
let content = if !self.paths.is_empty() && self.expanded {
|
||||
Some(
|
||||
v_flex()
|
||||
.relative()
|
||||
.ml_1p5()
|
||||
.px_1p5()
|
||||
.gap_0p5()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.children(self.paths.iter().enumerate().map(|(index, path)| {
|
||||
let path_clone = path.clone();
|
||||
let workspace_clone = workspace.clone();
|
||||
let button_label = path.to_string_lossy().to_string();
|
||||
|
||||
Button::new(("path", index), button_label)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.on_click(move |_, window, cx| {
|
||||
workspace_clone
|
||||
.update(cx, |workspace, cx| {
|
||||
let path = PathBuf::from(&path_clone);
|
||||
let Some(project_path) = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.find_project_path(&path, cx)
|
||||
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(
|
||||
language::Point::new(0, 0),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}))
|
||||
.into_any(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.mb_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
ToolCallCardHeader::new(IconName::SearchCode, matches_label)
|
||||
.with_code_path(glob_label)
|
||||
.disclosure_slot(
|
||||
Disclosure::new("path-search-disclosure", self.expanded)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.disabled(self.paths.is_empty())
|
||||
.on_click(cx.listener(move |this, _, _, _cx| {
|
||||
this.expanded = !this.expanded;
|
||||
})),
|
||||
),
|
||||
)
|
||||
.children(content)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for FindPathTool {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Agent
|
||||
}
|
||||
|
||||
fn sort_name() -> &'static str {
|
||||
"FindPathTool"
|
||||
}
|
||||
|
||||
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let successful_card = cx.new(|_| FindPathToolCard {
|
||||
paths: vec![
|
||||
PathBuf::from("src/main.rs"),
|
||||
PathBuf::from("src/lib.rs"),
|
||||
PathBuf::from("tests/test.rs"),
|
||||
],
|
||||
expanded: true,
|
||||
glob: "*.rs".to_string(),
|
||||
_receiver_task: None,
|
||||
});
|
||||
|
||||
let empty_card = cx.new(|_| FindPathToolCard {
|
||||
paths: Vec::new(),
|
||||
expanded: false,
|
||||
glob: "*.nonexistent".to_string(),
|
||||
_receiver_task: None,
|
||||
});
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.children(vec![example_group(vec![
|
||||
single_example(
|
||||
"With Paths",
|
||||
div()
|
||||
.size_full()
|
||||
.child(successful_card.update(cx, |tool, cx| {
|
||||
tool.render(
|
||||
&ToolUseStatus::Finished("".into()),
|
||||
window,
|
||||
WeakEntity::new_invalid(),
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
}))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"No Paths",
|
||||
div()
|
||||
.size_full()
|
||||
.child(empty_card.update(cx, |tool, cx| {
|
||||
tool.render(
|
||||
&ToolUseStatus::Finished("".into()),
|
||||
window,
|
||||
WeakEntity::new_invalid(),
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
}))
|
||||
.into_any_element(),
|
||||
),
|
||||
])])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use project::{FakeFs, Project};
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_path_tool(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
serde_json::json!({
|
||||
"apple": {
|
||||
"banana": {
|
||||
"carrot": "1",
|
||||
},
|
||||
"bandana": {
|
||||
"carbonara": "2",
|
||||
},
|
||||
"endive": "3"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let matches = cx
|
||||
.update(|cx| search_paths("root/**/car*", project.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
]
|
||||
);
|
||||
|
||||
let matches = cx
|
||||
.update(|cx| search_paths("**/car*", project.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
Fast file pattern matching tool that works with any codebase size
|
||||
Fast file path pattern matching tool that works with any codebase size
|
||||
|
||||
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
||||
- Returns matching file paths sorted alphabetically
|
||||
@@ -2,8 +2,8 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::OffsetRangeExt;
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language::{OffsetRangeExt, ParseStatus, Point};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{
|
||||
Project,
|
||||
@@ -13,13 +13,16 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp, fmt::Write, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::RangeExt;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GrepToolInput {
|
||||
/// A regex pattern to search for in the entire project. Note that the regex
|
||||
/// will be parsed by the Rust `regex` crate.
|
||||
///
|
||||
/// Do NOT specify a path here! This will only be matched against the code **content**.
|
||||
pub regex: String,
|
||||
|
||||
/// A glob pattern for the paths of files to include in the search.
|
||||
@@ -73,7 +76,7 @@ impl Tool for GrepTool {
|
||||
match serde_json::from_value::<GrepToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let page = input.page();
|
||||
let regex_str = MarkdownString::inline_code(&input.regex);
|
||||
let regex_str = MarkdownInlineCode(&input.regex);
|
||||
let case_info = if input.case_sensitive {
|
||||
" (case-sensitive)"
|
||||
} else {
|
||||
@@ -96,9 +99,11 @@ impl Tool for GrepTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
const MAX_ANCESTOR_LINES: u32 = 10;
|
||||
|
||||
let input = match serde_json::from_value::<GrepToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
@@ -137,7 +142,7 @@ impl Tool for GrepTool {
|
||||
|
||||
let results = project.update(cx, |project, cx| project.search(query, cx));
|
||||
|
||||
cx.spawn(async move|cx| {
|
||||
cx.spawn(async move |cx| {
|
||||
futures::pin_mut!(results);
|
||||
|
||||
let mut output = String::new();
|
||||
@@ -145,68 +150,113 @@ impl Tool for GrepTool {
|
||||
let mut matches_found = 0;
|
||||
let mut has_more_matches = false;
|
||||
|
||||
while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
|
||||
'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
|
||||
if ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.read_with(cx, |buffer, cx| -> Result<(), anyhow::Error> {
|
||||
if let Some(path) = buffer.file().map(|file| file.full_path(cx)) {
|
||||
let mut file_header_written = false;
|
||||
let mut ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let mut point_range = range.to_point(buffer);
|
||||
point_range.start.row =
|
||||
point_range.start.row.saturating_sub(CONTEXT_LINES);
|
||||
point_range.start.column = 0;
|
||||
point_range.end.row = cmp::min(
|
||||
buffer.max_point().row,
|
||||
point_range.end.row + CONTEXT_LINES,
|
||||
);
|
||||
point_range.end.column = buffer.line_len(point_range.end.row);
|
||||
point_range
|
||||
})
|
||||
.peekable();
|
||||
let (Some(path), mut parse_status) = buffer.read_with(cx, |buffer, cx| {
|
||||
(buffer.file().map(|file| file.full_path(cx)), buffer.parse_status())
|
||||
})? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
while let Some(mut range) = ranges.next() {
|
||||
if skips_remaining > 0 {
|
||||
skips_remaining -= 1;
|
||||
continue;
|
||||
|
||||
while *parse_status.borrow() != ParseStatus::Idle {
|
||||
parse_status.changed().await?;
|
||||
}
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
let mut ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let matched = range.to_point(&snapshot);
|
||||
let matched_end_line_len = snapshot.line_len(matched.end.row);
|
||||
let full_lines = Point::new(matched.start.row, 0)..Point::new(matched.end.row, matched_end_line_len);
|
||||
let symbols = snapshot.symbols_containing(matched.start, None);
|
||||
|
||||
if let Some(ancestor_node) = snapshot.syntax_ancestor(full_lines.clone()) {
|
||||
let full_ancestor_range = ancestor_node.byte_range().to_point(&snapshot);
|
||||
let end_row = full_ancestor_range.end.row.min(full_ancestor_range.start.row + MAX_ANCESTOR_LINES);
|
||||
let end_col = snapshot.line_len(end_row);
|
||||
let capped_ancestor_range = Point::new(full_ancestor_range.start.row, 0)..Point::new(end_row, end_col);
|
||||
|
||||
if capped_ancestor_range.contains_inclusive(&full_lines) {
|
||||
return (capped_ancestor_range, Some(full_ancestor_range), symbols)
|
||||
}
|
||||
}
|
||||
|
||||
// We'd already found a full page of matches, and we just found one more.
|
||||
if matches_found >= RESULTS_PER_PAGE {
|
||||
has_more_matches = true;
|
||||
return Ok(());
|
||||
}
|
||||
let mut matched = matched;
|
||||
matched.start.column = 0;
|
||||
matched.start.row =
|
||||
matched.start.row.saturating_sub(CONTEXT_LINES);
|
||||
matched.end.row = cmp::min(
|
||||
snapshot.max_point().row,
|
||||
matched.end.row + CONTEXT_LINES,
|
||||
);
|
||||
matched.end.column = snapshot.line_len(matched.end.row);
|
||||
|
||||
while let Some(next_range) = ranges.peek() {
|
||||
if range.end.row >= next_range.start.row {
|
||||
range.end = next_range.end;
|
||||
ranges.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(matched, None, symbols)
|
||||
})
|
||||
.peekable();
|
||||
|
||||
if !file_header_written {
|
||||
writeln!(output, "\n## Matches in {}", path.display())?;
|
||||
file_header_written = true;
|
||||
}
|
||||
let mut file_header_written = false;
|
||||
|
||||
let start_line = range.start.row + 1;
|
||||
let end_line = range.end.row + 1;
|
||||
writeln!(output, "\n### Lines {start_line}-{end_line}\n```")?;
|
||||
output.extend(buffer.text_for_range(range));
|
||||
output.push_str("\n```\n");
|
||||
while let Some((mut range, ancestor_range, parent_symbols)) = ranges.next(){
|
||||
if skips_remaining > 0 {
|
||||
skips_remaining -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
matches_found += 1;
|
||||
// We'd already found a full page of matches, and we just found one more.
|
||||
if matches_found >= RESULTS_PER_PAGE {
|
||||
has_more_matches = true;
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
while let Some((next_range, _, _)) = ranges.peek() {
|
||||
if range.end.row >= next_range.start.row {
|
||||
range.end = next_range.end;
|
||||
ranges.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})??;
|
||||
if !file_header_written {
|
||||
writeln!(output, "\n## Matches in {}", path.display())?;
|
||||
file_header_written = true;
|
||||
}
|
||||
|
||||
let end_row = range.end.row;
|
||||
output.push_str("\n### ");
|
||||
|
||||
if let Some(parent_symbols) = &parent_symbols {
|
||||
for symbol in parent_symbols {
|
||||
write!(output, "{} › ", symbol.text)?;
|
||||
}
|
||||
}
|
||||
|
||||
if range.start.row == end_row {
|
||||
writeln!(output, "L{}", range.start.row + 1)?;
|
||||
} else {
|
||||
writeln!(output, "L{}-{}", range.start.row + 1, end_row + 1)?;
|
||||
}
|
||||
|
||||
output.push_str("```\n");
|
||||
output.extend(snapshot.text_for_range(range));
|
||||
output.push_str("\n```\n");
|
||||
|
||||
if let Some(ancestor_range) = ancestor_range {
|
||||
if end_row < ancestor_range.end.row {
|
||||
let remaining_lines = ancestor_range.end.row - end_row;
|
||||
writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?;
|
||||
}
|
||||
}
|
||||
|
||||
matches_found += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if matches_found == 0 {
|
||||
@@ -230,13 +280,16 @@ mod tests {
|
||||
use super::*;
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher};
|
||||
use project::{FakeFs, Project};
|
||||
use settings::SettingsStore;
|
||||
use unindent::Unindent;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
@@ -324,6 +377,7 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
@@ -398,6 +452,290 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper function to set up a syntax test environment
|
||||
async fn setup_syntax_test(cx: &mut TestAppContext) -> Entity<Project> {
|
||||
use unindent::Unindent;
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
|
||||
// Create test file with syntax structures
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
serde_json::json!({
|
||||
"test_syntax.rs": r#"
|
||||
fn top_level_function() {
|
||||
println!("This is at the top level");
|
||||
}
|
||||
|
||||
mod feature_module {
|
||||
pub mod nested_module {
|
||||
pub fn nested_function(
|
||||
first_arg: String,
|
||||
second_arg: i32,
|
||||
) {
|
||||
println!("Function in nested module");
|
||||
println!("{first_arg}");
|
||||
println!("{second_arg}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MyStruct {
|
||||
field1: String,
|
||||
field2: i32,
|
||||
}
|
||||
|
||||
impl MyStruct {
|
||||
fn method_with_block() {
|
||||
let condition = true;
|
||||
if condition {
|
||||
println!("Inside if block");
|
||||
}
|
||||
}
|
||||
|
||||
fn long_function() {
|
||||
println!("Line 1");
|
||||
println!("Line 2");
|
||||
println!("Line 3");
|
||||
println!("Line 4");
|
||||
println!("Line 5");
|
||||
println!("Line 6");
|
||||
println!("Line 7");
|
||||
println!("Line 8");
|
||||
println!("Line 9");
|
||||
println!("Line 10");
|
||||
println!("Line 11");
|
||||
println!("Line 12");
|
||||
}
|
||||
}
|
||||
|
||||
trait Processor {
|
||||
fn process(&self, input: &str) -> String;
|
||||
}
|
||||
|
||||
impl Processor for MyStruct {
|
||||
fn process(&self, input: &str) -> String {
|
||||
format!("Processed: {}", input)
|
||||
}
|
||||
}
|
||||
"#.unindent().trim(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
project.update(cx, |project, _cx| {
|
||||
project.languages().add(rust_lang().into())
|
||||
});
|
||||
|
||||
project
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_top_level_function(cx: &mut TestAppContext) {
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line at the top level of the file
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "This is at the top level".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### fn top_level_function › L1-3
|
||||
```
|
||||
fn top_level_function() {
|
||||
println!("This is at the top level");
|
||||
}
|
||||
```
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_function_body(cx: &mut TestAppContext) {
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line inside a function body
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "Function in nested module".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### mod feature_module › pub mod nested_module › pub fn nested_function › L10-14
|
||||
```
|
||||
) {
|
||||
println!("Function in nested module");
|
||||
println!("{first_arg}");
|
||||
println!("{second_arg}");
|
||||
}
|
||||
```
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_function_args_and_body(cx: &mut TestAppContext) {
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line with a function argument
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "second_arg".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### mod feature_module › pub mod nested_module › pub fn nested_function › L7-14
|
||||
```
|
||||
pub fn nested_function(
|
||||
first_arg: String,
|
||||
second_arg: i32,
|
||||
) {
|
||||
println!("Function in nested module");
|
||||
println!("{first_arg}");
|
||||
println!("{second_arg}");
|
||||
}
|
||||
```
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_if_block(cx: &mut TestAppContext) {
|
||||
use unindent::Unindent;
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line inside an if block
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "Inside if block".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### impl MyStruct › fn method_with_block › L26-28
|
||||
```
|
||||
if condition {
|
||||
println!("Inside if block");
|
||||
}
|
||||
```
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_long_function_top(cx: &mut TestAppContext) {
|
||||
use unindent::Unindent;
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line in the middle of a long function - should show message about remaining lines
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "Line 5".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### impl MyStruct › fn long_function › L31-41
|
||||
```
|
||||
fn long_function() {
|
||||
println!("Line 1");
|
||||
println!("Line 2");
|
||||
println!("Line 3");
|
||||
println!("Line 4");
|
||||
println!("Line 5");
|
||||
println!("Line 6");
|
||||
println!("Line 7");
|
||||
println!("Line 8");
|
||||
println!("Line 9");
|
||||
println!("Line 10");
|
||||
```
|
||||
|
||||
3 lines remaining in ancestor node. Read the file to see all.
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_long_function_bottom(cx: &mut TestAppContext) {
|
||||
use unindent::Unindent;
|
||||
let project = setup_syntax_test(cx).await;
|
||||
|
||||
// Test: Line in the long function
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "Line 12".to_string(),
|
||||
include_pattern: Some("**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
let expected = r#"
|
||||
Found 1 matches:
|
||||
|
||||
## Matches in root/test_syntax.rs
|
||||
|
||||
### impl MyStruct › fn long_function › L41-45
|
||||
```
|
||||
println!("Line 10");
|
||||
println!("Line 11");
|
||||
println!("Line 12");
|
||||
}
|
||||
}
|
||||
```
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
async fn run_grep_tool(
|
||||
input: serde_json::Value,
|
||||
project: Entity<Project>,
|
||||
@@ -405,10 +743,16 @@ mod tests {
|
||||
) -> String {
|
||||
let tool = Arc::new(GrepTool);
|
||||
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
||||
let task = cx.update(|cx| tool.run(input, &[], project, action_log, cx));
|
||||
let task = cx.update(|cx| tool.run(input, &[], project, action_log, None, cx));
|
||||
|
||||
match task.output.await {
|
||||
Ok(result) => result,
|
||||
Ok(result) => {
|
||||
if cfg!(windows) {
|
||||
result.replace("root\\", "root/")
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
Err(e) => panic!("Failed to run grep tool: {}", e),
|
||||
}
|
||||
}
|
||||
@@ -421,4 +765,20 @@ mod tests {
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ListDirectoryToolInput {
|
||||
@@ -63,7 +63,7 @@ impl Tool for ListDirectoryTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let path = MarkdownString::inline_code(&input.path);
|
||||
let path = MarkdownInlineCode(&input.path);
|
||||
format!("List the {path} directory's contents")
|
||||
}
|
||||
Err(_) => "List directory".to_string(),
|
||||
@@ -76,6 +76,7 @@ impl Tool for ListDirectoryTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
Lists files and directories in a given path. Prefer the `grep` or `path_search` tools when searching the codebase.
|
||||
Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MovePathToolInput {
|
||||
@@ -61,8 +61,8 @@ impl Tool for MovePathTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<MovePathToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let src = MarkdownString::inline_code(&input.source_path);
|
||||
let dest = MarkdownString::inline_code(&input.destination_path);
|
||||
let src = MarkdownInlineCode(&input.source_path);
|
||||
let dest = MarkdownInlineCode(&input.destination_path);
|
||||
let src_path = Path::new(&input.source_path);
|
||||
let dest_path = Path::new(&input.destination_path);
|
||||
|
||||
@@ -71,7 +71,7 @@ impl Tool for MovePathTool {
|
||||
.and_then(|os_str| os_str.to_os_string().into_string().ok())
|
||||
{
|
||||
Some(filename) if src_path.parent() == dest_path.parent() => {
|
||||
let filename = MarkdownString::inline_code(&filename);
|
||||
let filename = MarkdownInlineCode(&filename);
|
||||
format!("Rename {src} to {filename}")
|
||||
}
|
||||
_ => {
|
||||
@@ -89,6 +89,7 @@ impl Tool for MovePathTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<MovePathToolInput>(input) {
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use chrono::{Local, Utc};
|
||||
use gpui::{App, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
@@ -59,6 +59,7 @@ impl Tool for NowTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
_cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input: NowToolInput = match serde_json::from_value(input) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownEscaped;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct OpenToolInput {
|
||||
@@ -41,7 +41,7 @@ impl Tool for OpenTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<OpenToolInput>(input.clone()) {
|
||||
Ok(input) => format!("Open `{}`", MarkdownString::escape(&input.path_or_url)),
|
||||
Ok(input) => format!("Open `{}`", MarkdownEscaped(&input.path_or_url)),
|
||||
Err(_) => "Open file or URL".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ impl Tool for OpenTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input: OpenToolInput = match serde_json::from_value(input) {
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp, fmt::Write as _, path::PathBuf, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::paths::PathMatcher;
|
||||
use worktree::Snapshot;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PathSearchToolInput {
|
||||
/// The glob to match against every path in the project.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following root directories:
|
||||
///
|
||||
/// - directory1/a/something.txt
|
||||
/// - directory2/a/things.txt
|
||||
/// - directory3/a/other.txt
|
||||
///
|
||||
/// You can get back the first two paths by providing a glob of "*thing*.txt"
|
||||
/// </example>
|
||||
pub glob: String,
|
||||
|
||||
/// Optional starting position for paginated results (0-based).
|
||||
/// When not provided, starts from the beginning.
|
||||
#[serde(default)]
|
||||
pub offset: u32,
|
||||
}
|
||||
|
||||
const RESULTS_PER_PAGE: usize = 50;
|
||||
|
||||
pub struct PathSearchTool;
|
||||
|
||||
impl Tool for PathSearchTool {
|
||||
fn name(&self) -> String {
|
||||
"path_search".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./path_search_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::SearchCode
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<PathSearchToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<PathSearchToolInput>(input.clone()) {
|
||||
Ok(input) => format!("Find paths matching “`{}`”", input.glob),
|
||||
Err(_) => "Search paths".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
|
||||
Ok(input) => (input.offset, input.glob),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
let offset = offset as usize;
|
||||
let task = search_paths(&glob, project, cx);
|
||||
cx.background_spawn(async move {
|
||||
let matches = task.await?;
|
||||
let paginated_matches = &matches[cmp::min(offset, matches.len())
|
||||
..cmp::min(offset + RESULTS_PER_PAGE, matches.len())];
|
||||
|
||||
if matches.is_empty() {
|
||||
Ok("No matches found".to_string())
|
||||
} else {
|
||||
let mut message = format!("Found {} total matches.", matches.len());
|
||||
if matches.len() > RESULTS_PER_PAGE {
|
||||
write!(
|
||||
&mut message,
|
||||
"\nShowing results {}-{} (provide 'offset' parameter for more results):",
|
||||
offset + 1,
|
||||
offset + paginated_matches.len()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
|
||||
write!(&mut message, "\n{}", mat.display()).unwrap();
|
||||
}
|
||||
Ok(message)
|
||||
}
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
|
||||
let path_matcher = match PathMatcher::new([
|
||||
// Sometimes models try to search for "". In this case, return all paths in the project.
|
||||
if glob.is_empty() { "*" } else { glob },
|
||||
]) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
|
||||
};
|
||||
let snapshots: Vec<Snapshot> = project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
Ok(snapshots
|
||||
.iter()
|
||||
.flat_map(|snapshot| {
|
||||
let root_name = PathBuf::from(snapshot.root_name());
|
||||
snapshot
|
||||
.entries(false, 0)
|
||||
.map(move |entry| root_name.join(&entry.path))
|
||||
.filter(|path| path_matcher.is_match(&path))
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use project::{FakeFs, Project};
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_path_search_tool(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
serde_json::json!({
|
||||
"apple": {
|
||||
"banana": {
|
||||
"carrot": "1",
|
||||
},
|
||||
"bandana": {
|
||||
"carbonara": "2",
|
||||
},
|
||||
"endive": "3"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let matches = cx
|
||||
.update(|cx| search_paths("root/**/car*", project.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
]
|
||||
);
|
||||
|
||||
let matches = cx
|
||||
.update(|cx| search_paths("**/car*", project.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
matches,
|
||||
&[
|
||||
PathBuf::from("root/apple/banana/carrot"),
|
||||
PathBuf::from("root/apple/bandana/carbonara")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::outline;
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
|
||||
use indoc::formatdoc;
|
||||
use itertools::Itertools;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
@@ -10,13 +12,9 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
/// If the model requests to read a file whose size exceeds this, then
|
||||
/// the tool will return an error along with the model's symbol outline,
|
||||
/// and suggest trying again using line ranges from the outline.
|
||||
const MAX_FILE_SIZE_TO_READ: usize = 16384;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ReadFileToolInput {
|
||||
/// The relative path of the file to read.
|
||||
@@ -39,7 +37,7 @@ pub struct ReadFileToolInput {
|
||||
#[serde(default)]
|
||||
pub start_line: Option<usize>,
|
||||
|
||||
/// Optional line number to end reading on (1-based index)
|
||||
/// Optional line number to end reading on (1-based index, inclusive)
|
||||
#[serde(default)]
|
||||
pub end_line: Option<usize>,
|
||||
}
|
||||
@@ -70,7 +68,7 @@ impl Tool for ReadFileTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let path = MarkdownString::inline_code(&input.path);
|
||||
let path = MarkdownInlineCode(&input.path);
|
||||
match (input.start_line, input.end_line) {
|
||||
(Some(start), None) => format!("Read file {path} (from line {start})"),
|
||||
(Some(start), Some(end)) => format!("Read file {path} (lines {start}-{end})"),
|
||||
@@ -87,6 +85,7 @@ impl Tool for ReadFileTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
|
||||
@@ -126,7 +125,7 @@ impl Tool for ReadFileTool {
|
||||
let start = input.start_line.unwrap_or(1);
|
||||
let lines = text.split('\n').skip(start - 1);
|
||||
if let Some(end) = input.end_line {
|
||||
let count = end.saturating_sub(start).max(1); // Ensure at least 1 line
|
||||
let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
|
||||
Itertools::intersperse(lines.take(count), "\n").collect()
|
||||
} else {
|
||||
Itertools::intersperse(lines, "\n").collect()
|
||||
@@ -142,7 +141,7 @@ impl Tool for ReadFileTool {
|
||||
// No line ranges specified, so check file size to see if it's too big.
|
||||
let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
|
||||
|
||||
if file_size <= MAX_FILE_SIZE_TO_READ {
|
||||
if file_size <= outline::AUTO_OUTLINE_SIZE {
|
||||
// File is small enough, so return its contents.
|
||||
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||
|
||||
@@ -152,9 +151,9 @@ impl Tool for ReadFileTool {
|
||||
|
||||
Ok(result)
|
||||
} else {
|
||||
// File is too big, so return an error with the outline
|
||||
// File is too big, so return the outline
|
||||
// and a suggestion to read again with line numbers.
|
||||
let outline = file_outline(project, file_path, action_log, None, cx).await?;
|
||||
let outline = outline::file_outline(project, file_path, action_log, None, cx).await?;
|
||||
Ok(formatdoc! {"
|
||||
This file was too big to read all at once. Here is an outline of its symbols:
|
||||
|
||||
@@ -193,7 +192,7 @@ mod test {
|
||||
"path": "root/nonexistent_file.txt"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -223,7 +222,7 @@ mod test {
|
||||
"path": "root/small_file.txt"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -253,7 +252,7 @@ mod test {
|
||||
"path": "root/large_file.rs"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log.clone(), cx)
|
||||
.run(input, &[], project.clone(), action_log.clone(), None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -277,7 +276,7 @@ mod test {
|
||||
"offset": 1
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -323,11 +322,11 @@ mod test {
|
||||
"end_line": 4
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, cx)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result.unwrap(), "Line 2\nLine 3");
|
||||
assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4");
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language::{self, Buffer, ToPointUtf16};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -87,6 +87,7 @@ impl Tool for RenameTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<RenameToolInput>(input) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AsyncApp, Entity, Task};
|
||||
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
|
||||
use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
@@ -8,7 +8,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write, ops::Range, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
use crate::schema::json_schema_for;
|
||||
|
||||
@@ -91,7 +91,7 @@ impl Tool for SymbolInfoTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<SymbolInfoToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let symbol = MarkdownString::inline_code(&input.symbol);
|
||||
let symbol = MarkdownInlineCode(&input.symbol);
|
||||
|
||||
match input.command {
|
||||
Info::Definition => {
|
||||
@@ -121,6 +121,7 @@ impl Tool for SymbolInfoTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user