Compare commits
97 Commits
vim-projec
...
surface-40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
525a5505a0 | ||
|
|
3180de3bc2 | ||
|
|
2c917cc05b | ||
|
|
525f65855e | ||
|
|
67e6071ebc | ||
|
|
145b8c31dc | ||
|
|
c2a4afdaef | ||
|
|
bf0dd4057c | ||
|
|
3c4ca3f372 | ||
|
|
03132921c7 | ||
|
|
c0fadae881 | ||
|
|
1c66c3991d | ||
|
|
7e591a7e9a | ||
|
|
c44d93745a | ||
|
|
b4e4e0d3ac | ||
|
|
097024d46f | ||
|
|
f1c2afdee0 | ||
|
|
ea120dfe18 | ||
|
|
d2988ffc77 | ||
|
|
f17d2c92b6 | ||
|
|
c1d9dc369c | ||
|
|
696fdd8fed | ||
|
|
980f8bff2a | ||
|
|
2a3bcbfe0f | ||
|
|
5225a84aff | ||
|
|
5c70f8391f | ||
|
|
10efbd5eb4 | ||
|
|
0386f240a9 | ||
|
|
a39ba03bcc | ||
|
|
2c7bcfcb7b | ||
|
|
6bea23e990 | ||
|
|
98da1ea169 | ||
|
|
98a83b47e6 | ||
|
|
5f356d04ff | ||
|
|
73d3f9611e | ||
|
|
d9cfc2c883 | ||
|
|
ee420d530e | ||
|
|
d801d0950e | ||
|
|
3f25d36b3c | ||
|
|
f015368586 | ||
|
|
4bf3b9d62e | ||
|
|
599a217ea5 | ||
|
|
b0a7defd09 | ||
|
|
57e3bcfcf8 | ||
|
|
b2f561165f | ||
|
|
fd1494c31a | ||
|
|
faa1136651 | ||
|
|
6bf5e92a25 | ||
|
|
46ad6c0bbb | ||
|
|
671500de1b | ||
|
|
0519c645fb | ||
|
|
23872b0523 | ||
|
|
4b050b651a | ||
|
|
bb46bc167a | ||
|
|
b274f80dd9 | ||
|
|
d77ab99ab1 | ||
|
|
97792f7fb9 | ||
|
|
9bebf314e0 | ||
|
|
4092e81ada | ||
|
|
e0b64773d9 | ||
|
|
f1bebd79d1 | ||
|
|
a66a539a09 | ||
|
|
a2d3e3baf9 | ||
|
|
175162af4f | ||
|
|
cdcc068906 | ||
|
|
86484aaded | ||
|
|
d32934a893 | ||
|
|
b463266fa1 | ||
|
|
b0525a26a6 | ||
|
|
1683052e6c | ||
|
|
07cc87b288 | ||
|
|
1277f328c4 | ||
|
|
b3097cfc8a | ||
|
|
305206fd48 | ||
|
|
c387203ac8 | ||
|
|
a260ba6428 | ||
|
|
a8e0de37ac | ||
|
|
a1a599dac5 | ||
|
|
524b97d729 | ||
|
|
8772727034 | ||
|
|
aaa116d129 | ||
|
|
c1096d8b63 | ||
|
|
092071a2f0 | ||
|
|
723f9b1371 | ||
|
|
37523b0007 | ||
|
|
b4167caaf1 | ||
|
|
020f518231 | ||
|
|
ead4f26b52 | ||
|
|
3de3a369f5 | ||
|
|
28a0b82618 | ||
|
|
e2c95a8d84 | ||
|
|
3da4d3aac3 | ||
|
|
6f99eeffa8 | ||
|
|
15ab96af6b | ||
|
|
e80b490ac0 | ||
|
|
3c577ba019 | ||
|
|
e1d295a6b4 |
6
.github/actions/run_tests/action.yml
vendored
6
.github/actions/run_tests/action.yml
vendored
@@ -4,10 +4,8 @@ description: "Runs the tests"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Rust
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
cargo install cargo-nextest --locked
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
|
||||
3
.github/actions/run_tests_windows/action.yml
vendored
3
.github/actions/run_tests_windows/action.yml
vendored
@@ -11,9 +11,8 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install test runner
|
||||
shell: powershell
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: cargo install cargo-nextest --locked
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
|
||||
2
.github/workflows/bump_patch_version.yml
vendored
2
.github/workflows/bump_patch_version.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
which cargo-set-version > /dev/null || cargo install cargo-edit
|
||||
which cargo-set-version > /dev/null || cargo install cargo-edit -f --no-default-features --features "set-version"
|
||||
output="$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')"
|
||||
export GIT_COMMITTER_NAME="Zed Bot"
|
||||
export GIT_COMMITTER_EMAIL="hi@zed.dev"
|
||||
|
||||
@@ -26,3 +26,4 @@ jobs:
|
||||
ascending: true
|
||||
enable-statistics: true
|
||||
stale-issue-label: "stale"
|
||||
exempt-issue-labels: "never stale"
|
||||
|
||||
3
.github/workflows/compare_perf.yml
vendored
3
.github/workflows/compare_perf.yml
vendored
@@ -39,8 +39,7 @@ jobs:
|
||||
run: ./script/download-wasi-sdk
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: compare_perf::run_perf::install_hyperfine
|
||||
run: cargo install hyperfine
|
||||
shell: bash -euxo pipefail {0}
|
||||
uses: taiki-e/install-action@hyperfine
|
||||
- name: steps::git_checkout
|
||||
run: git fetch origin ${{ inputs.base }} && git checkout ${{ inputs.base }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
4
.github/workflows/deploy_collab.yml
vendored
4
.github/workflows/deploy_collab.yml
vendored
@@ -43,9 +43,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install cargo nextest
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
cargo install cargo-nextest --locked
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Limit target directory size
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
37
Cargo.lock
generated
37
Cargo.lock
generated
@@ -322,6 +322,7 @@ dependencies = [
|
||||
"assistant_slash_command",
|
||||
"assistant_slash_commands",
|
||||
"assistant_text_thread",
|
||||
"async-fs",
|
||||
"audio",
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
@@ -343,6 +344,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"html_to_markdown",
|
||||
"http_client",
|
||||
"image",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"jsonschema",
|
||||
@@ -3688,6 +3690,7 @@ dependencies = [
|
||||
"collections",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"log",
|
||||
"net",
|
||||
"parking_lot",
|
||||
@@ -5312,6 +5315,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"supermaven",
|
||||
"sweep_ai",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -5861,6 +5865,7 @@ dependencies = [
|
||||
"lsp",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"proto",
|
||||
"semantic_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -14034,6 +14039,7 @@ dependencies = [
|
||||
"paths",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rayon",
|
||||
"release_channel",
|
||||
@@ -16586,6 +16592,33 @@ dependencies = [
|
||||
"zeno",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sweep_ai"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"brotli",
|
||||
"client",
|
||||
"collections",
|
||||
"edit_prediction",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"indoc",
|
||||
"language",
|
||||
"project",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tree-sitter-rust",
|
||||
"util",
|
||||
"workspace",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia"
|
||||
version = "0.5.5"
|
||||
@@ -21213,7 +21246,6 @@ dependencies = [
|
||||
"audio",
|
||||
"auto_update",
|
||||
"auto_update_ui",
|
||||
"backtrace",
|
||||
"bincode 1.3.3",
|
||||
"breadcrumbs",
|
||||
"call",
|
||||
@@ -21278,7 +21310,6 @@ dependencies = [
|
||||
"mimalloc",
|
||||
"miniprofiler_ui",
|
||||
"nc",
|
||||
"nix 0.29.0",
|
||||
"node_runtime",
|
||||
"notifications",
|
||||
"onboarding",
|
||||
@@ -21314,13 +21345,13 @@ dependencies = [
|
||||
"snippets_ui",
|
||||
"supermaven",
|
||||
"svg_preview",
|
||||
"sweep_ai",
|
||||
"sysinfo 0.37.2",
|
||||
"system_specs",
|
||||
"tab_switcher",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
"theme_extension",
|
||||
|
||||
19
Cargo.toml
19
Cargo.toml
@@ -165,6 +165,7 @@ members = [
|
||||
"crates/sum_tree",
|
||||
"crates/supermaven",
|
||||
"crates/supermaven_api",
|
||||
"crates/sweep_ai",
|
||||
"crates/codestral",
|
||||
"crates/svg_preview",
|
||||
"crates/system_specs",
|
||||
@@ -398,6 +399,7 @@ streaming_diff = { path = "crates/streaming_diff" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
supermaven_api = { path = "crates/supermaven_api" }
|
||||
sweep_ai = { path = "crates/sweep_ai" }
|
||||
codestral = { path = "crates/codestral" }
|
||||
system_specs = { path = "crates/system_specs" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
@@ -478,6 +480,7 @@ bitflags = "2.6.0"
|
||||
blade-graphics = { version = "0.7.0" }
|
||||
blade-macros = { version = "0.3.0" }
|
||||
blade-util = { version = "0.3.0" }
|
||||
brotli = "8.0.2"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
@@ -631,6 +634,7 @@ scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
||||
serde_derive = "1.0.221"
|
||||
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
|
||||
serde_json_lenient = { version = "0.2", features = [
|
||||
"preserve_order",
|
||||
@@ -724,6 +728,7 @@ yawc = "0.2.5"
|
||||
zeroize = "1.8"
|
||||
zstd = "0.11"
|
||||
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.61"
|
||||
features = [
|
||||
@@ -792,6 +797,19 @@ codegen-units = 16
|
||||
codegen-units = 16
|
||||
|
||||
[profile.dev.package]
|
||||
# proc-macros start
|
||||
gpui_macros = { opt-level = 3 }
|
||||
derive_refineable = { opt-level = 3 }
|
||||
settings_macros = { opt-level = 3 }
|
||||
sqlez_macros = { opt-level = 3, codegen-units = 1 }
|
||||
ui_macros = { opt-level = 3 }
|
||||
util_macros = { opt-level = 3 }
|
||||
serde_derive = { opt-level = 3 }
|
||||
quote = { opt-level = 3 }
|
||||
syn = { opt-level = 3 }
|
||||
proc-macro2 = { opt-level = 3 }
|
||||
# proc-macros end
|
||||
|
||||
taffy = { opt-level = 3 }
|
||||
cranelift-codegen = { opt-level = 3 }
|
||||
cranelift-codegen-meta = { opt-level = 3 }
|
||||
@@ -833,7 +851,6 @@ semantic_version = { codegen-units = 1 }
|
||||
session = { codegen-units = 1 }
|
||||
snippet = { codegen-units = 1 }
|
||||
snippets_ui = { codegen-units = 1 }
|
||||
sqlez_macros = { codegen-units = 1 }
|
||||
story = { codegen-units = 1 }
|
||||
supermaven_api = { codegen-units = 1 }
|
||||
telemetry_events = { codegen-units = 1 }
|
||||
|
||||
@@ -44,6 +44,7 @@ design
|
||||
|
||||
docs
|
||||
= @probably-neb
|
||||
= @miguelraz
|
||||
|
||||
extension
|
||||
= @kubkon
|
||||
@@ -98,6 +99,9 @@ settings_ui
|
||||
= @danilo-leal
|
||||
= @probably-neb
|
||||
|
||||
support
|
||||
= @miguelraz
|
||||
|
||||
tasks
|
||||
= @SomeoneToIgnore
|
||||
= @Veykril
|
||||
|
||||
32
assets/icons/sweep_ai.svg
Normal file
32
assets/icons/sweep_ai.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3348_16)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97419 6.27207C8.44653 6.29114 8.86622 6.27046 9.23628 6.22425C9.08884 7.48378 8.7346 8.72903 8.16697 9.90688C8.04459 9.83861 7.92582 9.76008 7.81193 9.67108C7.64539 9.54099 7.49799 9.39549 7.37015 9.23818C7.5282 9.54496 7.64901 9.86752 7.73175 10.1986C7.35693 10.6656 6.90663 11.0373 6.412 11.3101C5.01165 10.8075 4.03638 9.63089 4.03638 7.93001C4.03638 6.96185 4.35234 6.07053 4.88281 5.36157C5.34001 5.69449 6.30374 6.20455 7.97419 6.27207ZM8.27511 11.5815C10.3762 11.5349 11.8115 10.7826 12.8347 7.93001C11.6992 7.93001 11.4246 7.10731 11.1188 6.19149C11.0669 6.03596 11.0141 5.87771 10.956 5.72037C10.6733 5.86733 10.2753 6.02782 9.74834 6.13895C9.59658 7.49345 9.20592 8.83238 8.56821 10.0897C8.89933 10.2093 9.24674 10.262 9.5908 10.2502C9.08928 10.4803 8.62468 10.8066 8.22655 11.2255C8.2457 11.3438 8.26186 11.4625 8.27511 11.5815ZM6.62702 7.75422C6.62702 7.50604 6.82821 7.30485 7.07639 7.30485C7.32457 7.30485 7.52576 7.50604 7.52576 7.75422V8.23616C7.52576 8.48435 7.32457 8.68554 7.07639 8.68554C6.82821 8.68554 6.62702 8.48435 6.62702 8.23616V7.75422ZM5.27746 7.30485C5.05086 7.30485 4.86716 7.48854 4.86716 7.71513V8.27525C4.86716 8.50185 5.05086 8.68554 5.27746 8.68554C5.50406 8.68554 5.68776 8.50185 5.68776 8.27525V7.71513C5.68776 7.48854 5.50406 7.30485 5.27746 7.30485Z" fill="white"/>
|
||||
<mask id="mask0_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="4" y="5" width="9" height="7">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97419 6.27207C8.44653 6.29114 8.86622 6.27046 9.23628 6.22425C9.08884 7.48378 8.7346 8.72903 8.16697 9.90688C8.04459 9.83861 7.92582 9.76008 7.81193 9.67108C7.64539 9.54099 7.49799 9.39549 7.37015 9.23818C7.5282 9.54496 7.64901 9.86752 7.73175 10.1986C7.35693 10.6656 6.90663 11.0373 6.412 11.3101C5.01165 10.8075 4.03638 9.63089 4.03638 7.93001C4.03638 6.96185 4.35234 6.07053 4.88281 5.36157C5.34001 5.69449 6.30374 6.20455 7.97419 6.27207ZM8.27511 11.5815C10.3762 11.5349 11.8115 10.7826 12.8347 7.93001C11.6992 7.93001 11.4246 7.10731 11.1188 6.19149C11.0669 6.03596 11.0141 5.87771 10.956 5.72037C10.6733 5.86733 10.2753 6.02782 9.74834 6.13895C9.59658 7.49345 9.20592 8.83238 8.56821 10.0897C8.89933 10.2093 9.24674 10.262 9.5908 10.2502C9.08928 10.4803 8.62468 10.8066 8.22655 11.2255C8.2457 11.3438 8.26186 11.4625 8.27511 11.5815ZM6.62702 7.75422C6.62702 7.50604 6.82821 7.30485 7.07639 7.30485C7.32457 7.30485 7.52576 7.50604 7.52576 7.75422V8.23616C7.52576 8.48435 7.32457 8.68554 7.07639 8.68554C6.82821 8.68554 6.62702 8.48435 6.62702 8.23616V7.75422ZM5.27746 7.30485C5.05086 7.30485 4.86716 7.48854 4.86716 7.71513V8.27525C4.86716 8.50185 5.05086 8.68554 5.27746 8.68554C5.50406 8.68554 5.68776 8.50185 5.68776 8.27525V7.71513C5.68776 7.48854 5.50406 7.30485 5.27746 7.30485Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_3348_16)">
|
||||
<path d="M9.23617 6.22425L9.39588 6.24293L9.41971 6.0393L9.21624 6.06471L9.23617 6.22425ZM8.16687 9.90688L8.08857 10.0473L8.23765 10.1305L8.31174 9.97669L8.16687 9.90688ZM7.37005 9.23819L7.49487 9.13676L7.22714 9.3118L7.37005 9.23819ZM7.73165 10.1986L7.85702 10.2993L7.90696 10.2371L7.88761 10.1597L7.73165 10.1986ZM6.41189 11.3101L6.35758 11.4615L6.42594 11.486L6.48954 11.4509L6.41189 11.3101ZM4.88271 5.36157L4.97736 5.23159L4.84905 5.13817L4.75397 5.26525L4.88271 5.36157ZM8.27501 11.5815L8.11523 11.5993L8.13151 11.7456L8.27859 11.7423L8.27501 11.5815ZM12.8346 7.93001L12.986 7.98428L13.0631 7.76921H12.8346V7.93001ZM10.9559 5.72037L11.1067 5.66469L11.0436 5.49354L10.8817 5.5777L10.9559 5.72037ZM9.74824 6.13896L9.71508 5.98161L9.60139 6.0056L9.58846 6.12102L9.74824 6.13896ZM8.56811 10.0897L8.42469 10.017L8.34242 10.1792L8.51348 10.241L8.56811 10.0897ZM9.5907 10.2502L9.65775 10.3964L9.58519 10.0896L9.5907 10.2502ZM8.22644 11.2255L8.10992 11.1147L8.05502 11.1725L8.06773 11.2512L8.22644 11.2255ZM9.21624 6.06471C8.85519 6.10978 8.44439 6.13015 7.98058 6.11139L7.96756 6.43272C8.44852 6.45215 8.87701 6.43111 9.25607 6.3838L9.21624 6.06471ZM8.31174 9.97669C8.88724 8.78244 9.2464 7.51988 9.39588 6.24293L9.07647 6.20557C8.93108 7.44772 8.58175 8.67563 8.02203 9.83708L8.31174 9.97669ZM8.2452 9.76645C8.12998 9.70219 8.01817 9.62826 7.91082 9.54438L7.71285 9.79779C7.8333 9.8919 7.95895 9.97503 8.08857 10.0473L8.2452 9.76645ZM7.91082 9.54438C7.75387 9.4218 7.61512 9.28479 7.49487 9.13676L7.24526 9.33957C7.38066 9.50619 7.53671 9.66023 7.71285 9.79779L7.91082 9.54438ZM7.22714 9.3118C7.37944 9.60746 7.49589 9.91837 7.57564 10.2376L7.88761 10.1597C7.80196 9.81663 7.67679 9.48248 7.513 9.16453L7.22714 9.3118ZM7.60624 10.098C7.24483 10.5482 6.81083 10.9065 6.33425 11.1693L6.48954 11.4509C7.00223 11.1682 7.46887 10.7829 7.85702 10.2993L7.60624 10.098ZM3.87549 7.93001C3.87548 9.7042 4.89861 10.9378 6.35758 11.4615L6.46622 11.1588C5.12449 10.6772 4.19707 9.55763 4.19707 7.93001H3.87549ZM4.75397 5.26525C4.20309 6.00147 3.87549 6.92646 3.87549 7.93001H4.19707C4.19707 6.99724 4.50139 6.13959 5.01145 5.45791L4.75397 5.26525ZM7.98058 6.11139C6.34236 6.04516 5.40922 5.54604 4.97736 5.23159L4.78806 5.49157C5.27058 5.84291 6.26491 6.3639 7.96756 6.43272L7.98058 6.11139ZM8.27859 11.7423C9.34696 11.7185 10.2682 11.515 11.0542 10.9376C11.8388 10.3612 12.4683 9.4273 12.986 7.98428L12.6833 7.8757C12.1776 9.28534 11.5779 10.1539 10.8638 10.6784C10.1511 11.202 9.30417 11.3978 8.27143 11.4208L8.27859 11.7423ZM12.8346 7.76921C12.3148 7.76921 12.0098 7.58516 11.7925 7.30552C11.5639 7.0114 11.4266 6.60587 11.2712 6.14061L10.9662 6.24242C11.1166 6.69294 11.2695 7.15667 11.5385 7.50285C11.8188 7.86347 12.2189 8.09078 12.8346 8.09078V7.76921ZM11.2712 6.14061C11.2195 5.98543 11.1658 5.82478 11.1067 5.66469L10.805 5.77606C10.8621 5.93065 10.9142 6.0865 10.9662 6.24242L11.2712 6.14061ZM10.8817 5.5777C10.6115 5.71821 10.2273 5.87362 9.71508 5.98161L9.78143 6.29626C10.3232 6.18206 10.735 6.0165 11.0301 5.86301L10.8817 5.5777ZM9.58846 6.12102C9.43882 7.45684 9.05355 8.77717 8.42469 10.017L8.71149 10.1625C9.35809 8.88764 9.75417 7.53011 9.90806 6.15685L9.58846 6.12102ZM9.58519 10.0896C9.26119 10.1006 8.93423 10.051 8.62269 9.93854L8.51348 10.241C8.86427 10.3677 9.23205 10.4234 9.5962 10.4109L9.58519 10.0896ZM8.34301 11.3363C8.72675 10.9325 9.17443 10.6181 9.65775 10.3964L9.52365 10.1041C9.00392 10.3425 8.52241 10.6807 8.10992 11.1147L8.34301 11.3363ZM8.43483 11.5638C8.4213 11.4421 8.40475 11.3207 8.3852 11.1998L8.06773 11.2512C8.08644 11.3668 8.10225 11.4829 8.11523 11.5993L8.43483 11.5638ZM7.07629 7.14405C6.73931 7.14405 6.46613 7.41724 6.46613 7.75423H6.7877C6.7877 7.59484 6.91691 7.46561 7.07629 7.46561V7.14405ZM7.68646 7.75423C7.68646 7.41724 7.41326 7.14405 7.07629 7.14405V7.46561C7.23567 7.46561 7.36489 7.59484 7.36489 7.75423H7.68646ZM7.68646 8.23616V7.75423H7.36489V8.23616H7.68646ZM7.07629 8.84634C7.41326 8.84634 7.68646 8.57315 7.68646 8.23616H7.36489C7.36489 8.39555 7.23567 8.52474 7.07629 8.52474V8.84634ZM6.46613 8.23616C6.46613 8.57315 6.73931 8.84634 7.07629 8.84634V8.52474C6.91691 8.52474 6.7877 8.39555 6.7877 8.23616H6.46613ZM6.46613 7.75423V8.23616H6.7877V7.75423H6.46613ZM5.02785 7.71514C5.02785 7.57734 5.13956 7.46561 5.27736 7.46561V7.14405C4.96196 7.14405 4.70627 7.39974 4.70627 7.71514H5.02785ZM5.02785 8.27525V7.71514H4.70627V8.27525H5.02785ZM5.27736 8.52474C5.13956 8.52474 5.02785 8.41305 5.02785 8.27525H4.70627C4.70627 8.59065 4.96196 8.84634 5.27736 8.84634V8.52474ZM5.52687 8.27525C5.52687 8.41305 5.41516 8.52474 5.27736 8.52474V8.84634C5.59277 8.84634 5.84845 8.59065 5.84845 8.27525H5.52687ZM5.52687 7.71514V8.27525H5.84845V7.71514H5.52687ZM5.27736 7.46561C5.41516 7.46561 5.52687 7.57734 5.52687 7.71514H5.84845C5.84845 7.39974 5.59277 7.14405 5.27736 7.14405V7.46561Z" fill="white"/>
|
||||
</g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.12635 14.5901C7.22369 14.3749 7.3069 14.1501 7.37454 13.9167C7.54132 13.3412 7.5998 12.7599 7.56197 12.1948C7.53665 12.5349 7.47589 12.8775 7.37718 13.2181C7.23926 13.694 7.03667 14.1336 6.78174 14.5301C6.89605 14.5547 7.01101 14.5747 7.12635 14.5901Z" fill="white"/>
|
||||
<path d="M9.71984 7.74796C9.50296 7.74796 9.29496 7.83412 9.14159 7.98745C8.98822 8.14082 8.9021 8.34882 8.9021 8.5657C8.9021 8.78258 8.98822 8.99057 9.14159 9.14394C9.29496 9.29728 9.50296 9.38344 9.71984 9.38344V8.5657V7.74796Z" fill="white"/>
|
||||
<mask id="mask1_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="5" y="2" width="8" height="9">
|
||||
<path d="M12.3783 2.9985H5.36792V10.3954H12.3783V2.9985Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.75733 3.61999C9.98577 5.80374 9.60089 8.05373 8.56819 10.0898C8.43122 10.0403 8.29704 9.9794 8.16699 9.90688C9.15325 7.86033 9.49538 5.61026 9.22757 3.43526C9.39923 3.51584 9.57682 3.57729 9.75733 3.61999Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_3348_16)">
|
||||
<path d="M8.56815 10.0898L8.67689 10.1449L8.62812 10.241L8.52678 10.2044L8.56815 10.0898ZM9.75728 3.61998L9.78536 3.50136L9.86952 3.52127L9.87853 3.6073L9.75728 3.61998ZM8.16695 9.90687L8.1076 10.0133L8.00732 9.9574L8.05715 9.85398L8.16695 9.90687ZM9.22753 3.43524L9.10656 3.45014L9.07958 3.23116L9.27932 3.32491L9.22753 3.43524ZM8.45945 10.0346C9.48122 8.02009 9.86217 5.79374 9.63608 3.63266L9.87853 3.6073C10.1093 5.81372 9.72048 8.0873 8.67689 10.1449L8.45945 10.0346ZM8.22633 9.80041C8.35056 9.86971 8.47876 9.92791 8.60956 9.97514L8.52678 10.2044C8.38363 10.1527 8.24344 10.0891 8.1076 10.0133L8.22633 9.80041ZM9.34849 3.42035C9.61905 5.61792 9.27346 7.89158 8.27675 9.9598L8.05715 9.85398C9.03298 7.82905 9.37158 5.60258 9.10656 3.45014L9.34849 3.42035ZM9.72925 3.7386C9.54064 3.69399 9.3551 3.62977 9.17573 3.54558L9.27932 3.32491C9.44327 3.40188 9.61288 3.46058 9.78536 3.50136L9.72925 3.7386Z" fill="white"/>
|
||||
</g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4118 3.46925L11.2416 3.39926L11.1904 3.57611L11.349 3.62202C11.1904 3.57611 11.1904 3.57615 11.1904 3.5762L11.1903 3.57631L11.1902 3.57658L11.19 3.57741L11.1893 3.58009C11.1886 3.58233 11.1878 3.58548 11.1867 3.58949C11.1845 3.5975 11.1814 3.60897 11.1777 3.62359C11.1703 3.6528 11.1603 3.69464 11.1493 3.74656C11.1275 3.85017 11.102 3.99505 11.0869 4.16045C11.0573 4.4847 11.0653 4.91594 11.2489 5.26595C11.2613 5.28944 11.2643 5.31174 11.2625 5.32629C11.261 5.33849 11.2572 5.34226 11.2536 5.3449C11.0412 5.50026 10.5639 5.78997 9.76653 5.96607C9.76095 6.02373 9.75493 6.08134 9.74848 6.13895C10.601 5.95915 11.1161 5.65017 11.3511 5.4782C11.4413 5.41219 11.4471 5.28823 11.3952 5.18922C11.1546 4.73063 11.2477 4.08248 11.3103 3.78401C11.3314 3.68298 11.349 3.62202 11.349 3.62202C11.3745 3.6325 11.4002 3.63983 11.4259 3.64425C11.9083 3.72709 12.4185 2.78249 12.6294 2.33939C12.6852 2.22212 12.6234 2.08843 12.497 2.05837C11.2595 1.76399 5.46936 0.631807 4.57214 4.96989C4.55907 5.03307 4.57607 5.10106 4.62251 5.14584C4.87914 5.39322 5.86138 6.18665 7.9743 6.27207C8.44664 6.29114 8.86633 6.27046 9.23638 6.22425C9.24295 6.16797 9.24912 6.1117 9.25491 6.05534C8.88438 6.10391 8.46092 6.12641 7.98094 6.10702C5.91152 6.02337 4.96693 5.24843 4.73714 5.02692C4.73701 5.02679 4.73545 5.02525 4.73422 5.0208C4.73292 5.01611 4.73254 5.00987 4.73388 5.00334C4.94996 3.95861 5.4573 3.25195 6.11188 2.77714C6.77039 2.29947 7.58745 2.04983 8.42824 1.94075C10.1122 1.72228 11.8454 2.07312 12.4588 2.21906C12.4722 2.22225 12.4787 2.22927 12.4819 2.2362C12.4853 2.24342 12.4869 2.25443 12.4803 2.2684C12.3706 2.49879 12.183 2.85746 11.9656 3.13057C11.8564 3.26783 11.7479 3.37295 11.6469 3.43216C11.5491 3.48956 11.4752 3.49529 11.4118 3.46925Z" fill="white"/>
|
||||
<mask id="mask2_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="9" width="7" height="6">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.22654 11.2255C8.62463 10.8066 9.08923 10.4803 9.59075 10.2502C8.97039 10.2715 8.33933 10.0831 7.81189 9.67109C7.64534 9.541 7.49795 9.39549 7.37014 9.23819C7.52815 9.54497 7.64896 9.86752 7.7317 10.1986C6.70151 11.4821 5.1007 12.0466 3.57739 11.8125C3.85909 12.527 4.32941 13.178 4.97849 13.6851C5.8625 14.3756 6.92544 14.6799 7.96392 14.6227C8.32513 13.5174 8.4085 12.351 8.22654 11.2255Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask2_3348_16)">
|
||||
<path d="M9.59085 10.2502L9.58389 10.0472L9.67556 10.4349L9.59085 10.2502ZM8.22663 11.2255L8.02607 11.258L8.00999 11.1585L8.07936 11.0856L8.22663 11.2255ZM7.37024 9.23819L7.18961 9.33119L7.52789 9.11006L7.37024 9.23819ZM7.7318 10.1986L7.92886 10.1494L7.95328 10.2472L7.8902 10.3258L7.7318 10.1986ZM3.57749 11.8125L3.3885 11.887L3.25879 11.5579L3.60835 11.6117L3.57749 11.8125ZM7.96402 14.6227L8.15711 14.6858L8.11397 14.8179L7.97519 14.8255L7.96402 14.6227ZM9.67556 10.4349C9.19708 10.6544 8.7538 10.9657 8.37387 11.3655L8.07936 11.0856C8.49566 10.6475 8.98161 10.3062 9.50614 10.0656L9.67556 10.4349ZM7.93704 9.51099C8.42551 9.89261 9.00942 10.0669 9.58389 10.0472L9.59781 10.4533C8.93151 10.4761 8.25334 10.2737 7.68693 9.83118L7.93704 9.51099ZM7.52789 9.11006C7.64615 9.25565 7.78261 9.39038 7.93704 9.51099L7.68693 9.83118C7.50827 9.69161 7.34994 9.53537 7.21254 9.36627L7.52789 9.11006ZM7.5347 10.2479C7.45573 9.93178 7.34043 9.62393 7.18961 9.33119L7.55082 9.14514C7.71611 9.466 7.84242 9.80326 7.92886 10.1494L7.5347 10.2479ZM3.60835 11.6117C5.06278 11.8352 6.59038 11.2962 7.57335 10.0715L7.8902 10.3258C6.81284 11.6681 5.1388 12.258 3.54663 12.0133L3.60835 11.6117ZM4.85352 13.8452C4.17512 13.3152 3.68312 12.6343 3.3885 11.887L3.76648 11.738C4.03524 12.4197 4.4839 13.0409 5.10364 13.525L4.85352 13.8452ZM7.97519 14.8255C6.8895 14.8853 5.77774 14.5672 4.85352 13.8452L5.10364 13.525C5.94745 14.1842 6.96157 14.4744 7.95285 14.4198L7.97519 14.8255ZM8.42716 11.1931C8.61419 12.3499 8.52858 13.5491 8.15711 14.6858L7.77093 14.5596C8.12191 13.4857 8.20296 12.352 8.02607 11.258L8.42716 11.1931Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3348_16">
|
||||
<rect width="9.63483" height="14" fill="white" transform="translate(3.19995 1.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -313,7 +313,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewTextThread",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
"cmd-alt-n": "agent::NewExternalAgentThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -421,12 +421,6 @@
|
||||
"ctrl-[": "editor::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == helix_select && !menu",
|
||||
"bindings": {
|
||||
"escape": "vim::SwitchToHelixNormalMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
|
||||
"bindings": {
|
||||
|
||||
@@ -742,6 +742,16 @@
|
||||
// "never"
|
||||
"show": "always"
|
||||
},
|
||||
// Sort order for entries in the project panel.
|
||||
// This setting can take three values:
|
||||
//
|
||||
// 1. Show directories first, then files:
|
||||
// "directories_first"
|
||||
// 2. Mix directories and files together:
|
||||
// "mixed"
|
||||
// 3. Show files first, then directories:
|
||||
// "files_first"
|
||||
"sort_mode": "directories_first",
|
||||
// Whether to enable drag-and-drop operations in the project panel.
|
||||
"drag_and_drop": true,
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
|
||||
@@ -150,6 +150,7 @@ impl DbThread {
|
||||
.unwrap_or_default(),
|
||||
input: tool_use.input,
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1108,6 +1108,7 @@ fn tool_use(
|
||||
raw_input: serde_json::to_string_pretty(&input).unwrap(),
|
||||
input: serde_json::to_value(input).unwrap(),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -274,6 +274,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
fake_model
|
||||
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
|
||||
@@ -461,6 +462,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
));
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
@@ -470,6 +472,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
@@ -520,6 +523,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
@@ -554,6 +558,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
@@ -592,6 +597,7 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
@@ -621,6 +627,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
|
||||
raw_input: "{}".into(),
|
||||
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
fake_model
|
||||
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
|
||||
@@ -731,6 +738,7 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
|
||||
raw_input: "{}".into(),
|
||||
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
let tool_result = LanguageModelToolResult {
|
||||
tool_use_id: "tool_id_1".into(),
|
||||
@@ -1037,6 +1045,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
@@ -1080,6 +1089,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
|
||||
raw_input: json!({"text": "mcp"}).to_string(),
|
||||
input: json!({"text": "mcp"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
));
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
@@ -1089,6 +1099,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
|
||||
raw_input: json!({"text": "native"}).to_string(),
|
||||
input: json!({"text": "native"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
@@ -1788,6 +1799,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
let echo_tool_use = LanguageModelToolUse {
|
||||
id: "tool_id_2".into(),
|
||||
@@ -1795,6 +1807,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
fake_model.send_last_completion_stream_text_chunk("Hi!");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
@@ -2000,6 +2013,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
raw_input: input.to_string(),
|
||||
input,
|
||||
is_input_complete: false,
|
||||
thought_signature: None,
|
||||
},
|
||||
));
|
||||
|
||||
@@ -2012,6 +2026,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
raw_input: input.to_string(),
|
||||
input,
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
@@ -2214,6 +2229,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
|
||||
tool_use_1.clone(),
|
||||
|
||||
@@ -607,6 +607,8 @@ pub struct Thread {
|
||||
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||
pub(crate) project: Entity<Project>,
|
||||
pub(crate) action_log: Entity<ActionLog>,
|
||||
/// Tracks the last time files were read by the agent, to detect external modifications
|
||||
pub(crate) file_read_times: HashMap<PathBuf, fs::MTime>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
@@ -665,6 +667,7 @@ impl Thread {
|
||||
prompt_capabilities_rx,
|
||||
project,
|
||||
action_log,
|
||||
file_read_times: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,6 +863,7 @@ impl Thread {
|
||||
updated_at: db_thread.updated_at,
|
||||
prompt_capabilities_tx,
|
||||
prompt_capabilities_rx,
|
||||
file_read_times: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -999,6 +1003,7 @@ impl Thread {
|
||||
self.add_tool(NowTool);
|
||||
self.add_tool(OpenTool::new(self.project.clone()));
|
||||
self.add_tool(ReadFileTool::new(
|
||||
cx.weak_entity(),
|
||||
self.project.clone(),
|
||||
self.action_log.clone(),
|
||||
));
|
||||
@@ -1247,9 +1252,17 @@ impl Thread {
|
||||
log::trace!("Received completion event: {:?}", event);
|
||||
match event {
|
||||
Ok(event) => {
|
||||
tool_results.extend(this.update(cx, |this, cx| {
|
||||
this.handle_completion_event(event, event_stream, cx)
|
||||
})??);
|
||||
match this.update(cx, |this, cx| {
|
||||
this.handle_completion_event(event, event_stream, model.clone(), cx)
|
||||
})? {
|
||||
Ok(answer) => {
|
||||
tool_results.extend(answer);
|
||||
}
|
||||
Err(err) => {
|
||||
error = Some(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error = Some(err);
|
||||
@@ -1378,8 +1391,10 @@ impl Thread {
|
||||
&mut self,
|
||||
event: LanguageModelCompletionEvent,
|
||||
event_stream: &ThreadEventStream,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<Option<Task<LanguageModelToolResult>>> {
|
||||
) -> std::result::Result<Option<Task<LanguageModelToolResult>>, LanguageModelCompletionError>
|
||||
{
|
||||
log::trace!("Handling streamed completion event: {:?}", event);
|
||||
use LanguageModelCompletionEvent::*;
|
||||
|
||||
@@ -1416,8 +1431,8 @@ impl Thread {
|
||||
"Agent Thread Completion Usage Updated",
|
||||
thread_id = self.id.to_string(),
|
||||
prompt_id = self.prompt_id.to_string(),
|
||||
model = self.model.as_ref().map(|m| m.telemetry_id()),
|
||||
model_provider = self.model.as_ref().map(|m| m.provider_id().to_string()),
|
||||
model = model.telemetry_id(),
|
||||
model_provider = model.provider_id().to_string(),
|
||||
input_tokens = usage.input_tokens,
|
||||
output_tokens = usage.output_tokens,
|
||||
cache_creation_input_tokens = usage.cache_creation_input_tokens,
|
||||
@@ -1429,16 +1444,31 @@ impl Thread {
|
||||
self.update_model_request_usage(amount, limit, cx);
|
||||
}
|
||||
StatusUpdate(
|
||||
CompletionRequestStatus::Started
|
||||
| CompletionRequestStatus::Queued { .. }
|
||||
| CompletionRequestStatus::Failed { .. },
|
||||
) => {}
|
||||
CompletionRequestStatus::Started | CompletionRequestStatus::Queued { .. },
|
||||
) => {
|
||||
// No action needed for starting or queueing
|
||||
}
|
||||
StatusUpdate(CompletionRequestStatus::Failed {
|
||||
code,
|
||||
message,
|
||||
request_id,
|
||||
retry_after,
|
||||
}) => {
|
||||
return Err(LanguageModelCompletionError::from_cloud_failure(
|
||||
model.provider_name(),
|
||||
code,
|
||||
message,
|
||||
retry_after.map(Duration::from_secs_f64),
|
||||
));
|
||||
}
|
||||
StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => {
|
||||
self.tool_use_limit_reached = true;
|
||||
}
|
||||
Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()),
|
||||
Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()),
|
||||
Stop(StopReason::ToolUse | StopReason::EndTurn) => {}
|
||||
Stop(StopReason::Refusal) => return Err(LanguageModelCompletionError::Refusal),
|
||||
Stop(StopReason::MaxTokens) => return Err(LanguageModelCompletionError::MaxTokens),
|
||||
Stop(StopReason::ToolUse | StopReason::EndTurn) => {
|
||||
// Tool use will be handled separately, and ending the turn needs no action
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -2128,6 +2158,14 @@ impl Thread {
|
||||
delay: BASE_RETRY_DELAY,
|
||||
max_attempts: 2,
|
||||
}),
|
||||
Refusal => {
|
||||
// If the model refused the request, we can reasonably assume it will refuse on retry too.
|
||||
None
|
||||
}
|
||||
MaxTokens => {
|
||||
// Retrying won't help with having exceeded max tokens
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +309,40 @@ impl AgentTool for EditFileTool {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
// Check if the file has been modified since the agent last read it
|
||||
if let Some(abs_path) = abs_path.as_ref() {
|
||||
let (last_read_mtime, current_mtime, is_dirty) = self.thread.update(cx, |thread, cx| {
|
||||
let last_read = thread.file_read_times.get(abs_path).copied();
|
||||
let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime());
|
||||
let dirty = buffer.read(cx).is_dirty();
|
||||
(last_read, current, dirty)
|
||||
})?;
|
||||
|
||||
// Check for unsaved changes first - these indicate modifications we don't know about
|
||||
if is_dirty {
|
||||
anyhow::bail!(
|
||||
"This file cannot be written to because it has unsaved changes. \
|
||||
Please end the current conversation immediately by telling the user you want to write to this file (mention its path explicitly) but you can't write to it because it has unsaved changes. \
|
||||
Ask the user to save that buffer's changes and to inform you when it's ok to proceed."
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the file was modified on disk since we last read it
|
||||
if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) {
|
||||
// MTime can be unreliable for comparisons, so our newtype intentionally
|
||||
// doesn't support comparing them. If the mtime at all different
|
||||
// (which could be because of a modification or because e.g. system clock changed),
|
||||
// we pessimistically assume it was modified.
|
||||
if current != last_read {
|
||||
anyhow::bail!(
|
||||
"The file {} has been modified since you last read it. \
|
||||
Please read the file again to get the current state before editing it.",
|
||||
input.path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
||||
event_stream.update_diff(diff.clone());
|
||||
let _finalize_diff = util::defer({
|
||||
@@ -421,6 +455,17 @@ impl AgentTool for EditFileTool {
|
||||
log.buffer_edited(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
// Update the recorded read time after a successful edit so consecutive edits work
|
||||
if let Some(abs_path) = abs_path.as_ref() {
|
||||
if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| {
|
||||
buffer.file().and_then(|file| file.disk_state().mtime())
|
||||
})? {
|
||||
self.thread.update(cx, |thread, _| {
|
||||
thread.file_read_times.insert(abs_path.to_path_buf(), new_mtime);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let (new_text, unified_diff) = cx
|
||||
.background_spawn({
|
||||
@@ -1748,10 +1793,426 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_read_times_tracking(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
// Initially, file_read_times should be empty
|
||||
let is_empty = thread.read_with(cx, |thread, _| thread.file_read_times.is_empty());
|
||||
assert!(is_empty, "file_read_times should start empty");
|
||||
|
||||
// Create read tool
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
|
||||
// Read the file to record the read time
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that file_read_times now contains an entry for the file
|
||||
let has_entry = thread.read_with(cx, |thread, _| {
|
||||
thread.file_read_times.len() == 1
|
||||
&& thread
|
||||
.file_read_times
|
||||
.keys()
|
||||
.any(|path| path.ends_with("test.txt"))
|
||||
});
|
||||
assert!(
|
||||
has_entry,
|
||||
"file_read_times should contain an entry after reading the file"
|
||||
);
|
||||
|
||||
// Read the file again - should update the entry
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Should still have exactly one entry
|
||||
let has_one_entry = thread.read_with(cx, |thread, _| thread.file_read_times.len() == 1);
|
||||
assert!(
|
||||
has_one_entry,
|
||||
"file_read_times should still have one entry after re-reading"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_consecutive_edits_work(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let languages = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
let edit_tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Read the file first
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// First edit should work
|
||||
let edit_result = {
|
||||
let edit_task = cx.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "First edit".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
model.send_last_completion_stream_text_chunk(
|
||||
"<old_text>original content</old_text><new_text>modified content</new_text>"
|
||||
.to_string(),
|
||||
);
|
||||
model.end_last_completion_stream();
|
||||
|
||||
edit_task.await
|
||||
};
|
||||
assert!(
|
||||
edit_result.is_ok(),
|
||||
"First edit should succeed, got error: {:?}",
|
||||
edit_result.as_ref().err()
|
||||
);
|
||||
|
||||
// Second edit should also work because the edit updated the recorded read time
|
||||
let edit_result = {
|
||||
let edit_task = cx.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "Second edit".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
model.send_last_completion_stream_text_chunk(
|
||||
"<old_text>modified content</old_text><new_text>further modified content</new_text>".to_string(),
|
||||
);
|
||||
model.end_last_completion_stream();
|
||||
|
||||
edit_task.await
|
||||
};
|
||||
assert!(
|
||||
edit_result.is_ok(),
|
||||
"Second consecutive edit should succeed, got error: {:?}",
|
||||
edit_result.as_ref().err()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_external_modification_detected(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let languages = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
let edit_tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Read the file first
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Simulate external modification - advance time and save file
|
||||
cx.background_executor
|
||||
.advance_clock(std::time::Duration::from_secs(2));
|
||||
fs.save(
|
||||
path!("/root/test.txt").as_ref(),
|
||||
&"externally modified content".into(),
|
||||
language::LineEnding::Unix,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Reload the buffer to pick up the new mtime
|
||||
let project_path = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.find_project_path("root/test.txt", cx)
|
||||
})
|
||||
.expect("Should find project path");
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
buffer
|
||||
.update(cx, |buffer, cx| buffer.reload(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Try to edit - should fail because file was modified externally
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit after external change".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Edit should fail after external modification"
|
||||
);
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("has been modified since you last read it"),
|
||||
"Error should mention file modification, got: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dirty_buffer_detected(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let languages = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
let edit_tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Read the file first
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Open the buffer and make it dirty by editing without saving
|
||||
let project_path = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.find_project_path("root/test.txt", cx)
|
||||
})
|
||||
.expect("Should find project path");
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make an in-memory edit to the buffer (making it dirty)
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let end_point = buffer.max_point();
|
||||
buffer.edit([(end_point..end_point, " added text")], None, cx);
|
||||
});
|
||||
|
||||
// Verify buffer is dirty
|
||||
let is_dirty = buffer.read_with(cx, |buffer, _| buffer.is_dirty());
|
||||
assert!(is_dirty, "Buffer should be dirty after in-memory edit");
|
||||
|
||||
// Try to edit - should fail because buffer has unsaved changes
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit with dirty buffer".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_err(), "Edit should fail when buffer is dirty");
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("cannot be written to because it has unsaved changes"),
|
||||
"Error should mention unsaved changes, got: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, ToolCallUpdateFields};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use gpui::{App, Entity, SharedString, Task, WeakEntity};
|
||||
use indoc::formatdoc;
|
||||
use language::Point;
|
||||
use language_model::{LanguageModelImage, LanguageModelToolResultContent};
|
||||
@@ -12,7 +12,7 @@ use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
|
||||
use crate::{AgentTool, ToolCallEventStream, outline};
|
||||
use crate::{AgentTool, Thread, ToolCallEventStream, outline};
|
||||
|
||||
/// Reads the content of the given file in the project.
|
||||
///
|
||||
@@ -42,13 +42,19 @@ pub struct ReadFileToolInput {
|
||||
}
|
||||
|
||||
pub struct ReadFileTool {
|
||||
thread: WeakEntity<Thread>,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
}
|
||||
|
||||
impl ReadFileTool {
|
||||
pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
|
||||
pub fn new(
|
||||
thread: WeakEntity<Thread>,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
project,
|
||||
action_log,
|
||||
}
|
||||
@@ -195,6 +201,17 @@ impl AgentTool for ReadFileTool {
|
||||
anyhow::bail!("{file_path} not found");
|
||||
}
|
||||
|
||||
// Record the file read time and mtime
|
||||
if let Some(mtime) = buffer.read_with(cx, |buffer, _| {
|
||||
buffer.file().and_then(|file| file.disk_state().mtime())
|
||||
})? {
|
||||
self.thread
|
||||
.update(cx, |thread, _| {
|
||||
thread.file_read_times.insert(abs_path.to_path_buf(), mtime);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
let mut anchor = None;
|
||||
|
||||
// Check if specific line ranges are provided
|
||||
@@ -285,11 +302,15 @@ impl AgentTool for ReadFileTool {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{ContextServerRegistry, Templates, Thread};
|
||||
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use project::{FakeFs, Project};
|
||||
use prompt_store::ProjectContext;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -300,7 +321,20 @@ mod test {
|
||||
fs.insert_tree(path!("/root"), json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let (event_stream, _) = ToolCallEventStream::test();
|
||||
|
||||
let result = cx
|
||||
@@ -333,7 +367,20 @@ mod test {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = ReadFileToolInput {
|
||||
@@ -363,7 +410,20 @@ mod test {
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(Arc::new(rust_lang()));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = ReadFileToolInput {
|
||||
@@ -435,7 +495,20 @@ mod test {
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = ReadFileToolInput {
|
||||
@@ -463,7 +536,20 @@ mod test {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
|
||||
// start_line of 0 should be treated as 1
|
||||
let result = cx
|
||||
@@ -607,7 +693,20 @@ mod test {
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
|
||||
// Reading a file outside the project worktree should fail
|
||||
let result = cx
|
||||
@@ -821,7 +920,24 @@ mod test {
|
||||
.await;
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
));
|
||||
|
||||
// Test reading allowed files in worktree1
|
||||
let result = cx
|
||||
|
||||
@@ -247,37 +247,58 @@ impl AgentConnection for AcpConnection {
|
||||
let default_mode = self.default_mode.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||
let mcp_servers = if project.read(cx).is_local() {
|
||||
context_server_store
|
||||
.configured_server_ids()
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let configuration = context_server_store.configuration_for_server(id)?;
|
||||
let command = configuration.command();
|
||||
Some(acp::McpServer::Stdio {
|
||||
name: id.0.to_string(),
|
||||
command: command.path.clone(),
|
||||
args: command.args.clone(),
|
||||
env: if let Some(env) = command.env.as_ref() {
|
||||
env.iter()
|
||||
.map(|(name, value)| acp::EnvVariable {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
meta: None,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
let mcp_servers =
|
||||
if project.read(cx).is_local() {
|
||||
context_server_store
|
||||
.configured_server_ids()
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let configuration = context_server_store.configuration_for_server(id)?;
|
||||
match &*configuration {
|
||||
project::context_server_store::ContextServerConfiguration::Custom {
|
||||
command,
|
||||
..
|
||||
}
|
||||
| project::context_server_store::ContextServerConfiguration::Extension {
|
||||
command,
|
||||
..
|
||||
} => Some(acp::McpServer::Stdio {
|
||||
name: id.0.to_string(),
|
||||
command: command.path.clone(),
|
||||
args: command.args.clone(),
|
||||
env: if let Some(env) = command.env.as_ref() {
|
||||
env.iter()
|
||||
.map(|(name, value)| acp::EnvVariable {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
meta: None,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
}),
|
||||
project::context_server_store::ContextServerConfiguration::Http {
|
||||
url,
|
||||
headers,
|
||||
} => Some(acp::McpServer::Http {
|
||||
name: id.0.to_string(),
|
||||
url: url.to_string(),
|
||||
headers: headers.iter().map(|(name, value)| acp::HttpHeader {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
meta: None,
|
||||
}).collect(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
// In SSH projects, the external agent is running on the remote
|
||||
// machine, and currently we only run MCP servers on the local
|
||||
// machine. So don't pass any MCP servers to the agent in that case.
|
||||
Vec::new()
|
||||
};
|
||||
.collect()
|
||||
} else {
|
||||
// In SSH projects, the external agent is running on the remote
|
||||
// machine, and currently we only run MCP servers on the local
|
||||
// machine. So don't pass any MCP servers to the agent in that case.
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let response = conn
|
||||
|
||||
@@ -98,6 +98,8 @@ util.workspace = true
|
||||
watch.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
image.workspace = true
|
||||
async-fs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
acp_thread = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -28,6 +28,7 @@ use gpui::{
|
||||
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
|
||||
Subscription, Task, TextStyle, WeakEntity, pulsating_between,
|
||||
};
|
||||
use itertools::Either;
|
||||
use language::{Buffer, Language, language_settings::InlayHintKind};
|
||||
use language_model::LanguageModelImage;
|
||||
use postage::stream::Stream as _;
|
||||
@@ -912,74 +913,114 @@ impl MessageEditor {
|
||||
if !self.prompt_capabilities.borrow().image {
|
||||
return;
|
||||
}
|
||||
|
||||
let images = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| {
|
||||
item.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::Image(image) = entry {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if images.is_empty() {
|
||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||
return;
|
||||
}
|
||||
cx.stop_propagation();
|
||||
};
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
use itertools::Itertools;
|
||||
let (mut images, paths) = clipboard
|
||||
.into_entries()
|
||||
.filter_map(|entry| match entry {
|
||||
ClipboardEntry::Image(image) => Some(Either::Left(image)),
|
||||
ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
|
||||
_ => None,
|
||||
})
|
||||
.partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
|
||||
|
||||
let replacement_text = MentionUri::PastedImage.as_link().to_string();
|
||||
for image in images {
|
||||
let (excerpt_id, text_anchor, multibuffer_anchor) =
|
||||
self.editor.update(cx, |message_editor, cx| {
|
||||
let snapshot = message_editor.snapshot(window, cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.buffer_snapshot().as_singleton().unwrap();
|
||||
if !paths.is_empty() {
|
||||
images.extend(
|
||||
cx.background_spawn(async move {
|
||||
let mut images = vec![];
|
||||
for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
|
||||
let Ok(content) = async_fs::read(path).await else {
|
||||
continue;
|
||||
};
|
||||
let Ok(format) = image::guess_format(&content) else {
|
||||
continue;
|
||||
};
|
||||
images.push(gpui::Image::from_bytes(
|
||||
match format {
|
||||
image::ImageFormat::Png => gpui::ImageFormat::Png,
|
||||
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
|
||||
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
|
||||
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
|
||||
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
|
||||
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
|
||||
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
|
||||
_ => continue,
|
||||
},
|
||||
content,
|
||||
));
|
||||
}
|
||||
images
|
||||
})
|
||||
.await,
|
||||
);
|
||||
}
|
||||
|
||||
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
|
||||
let multibuffer_anchor = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_in_excerpt(*excerpt_id, text_anchor);
|
||||
message_editor.edit(
|
||||
[(
|
||||
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
|
||||
format!("{replacement_text} "),
|
||||
)],
|
||||
if images.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let replacement_text = MentionUri::PastedImage.as_link().to_string();
|
||||
let Ok(editor) = this.update(cx, |this, cx| {
|
||||
cx.stop_propagation();
|
||||
this.editor.clone()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
for image in images {
|
||||
let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
|
||||
editor.update_in(cx, |message_editor, window, cx| {
|
||||
let snapshot = message_editor.snapshot(window, cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.buffer_snapshot().as_singleton().unwrap();
|
||||
|
||||
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
|
||||
let multibuffer_anchor = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_in_excerpt(*excerpt_id, text_anchor);
|
||||
message_editor.edit(
|
||||
[(
|
||||
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
|
||||
format!("{replacement_text} "),
|
||||
)],
|
||||
cx,
|
||||
);
|
||||
(*excerpt_id, text_anchor, multibuffer_anchor)
|
||||
})
|
||||
else {
|
||||
break;
|
||||
};
|
||||
|
||||
let content_len = replacement_text.len();
|
||||
let Some(start_anchor) = multibuffer_anchor else {
|
||||
continue;
|
||||
};
|
||||
let Ok(end_anchor) = editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let image = Arc::new(image);
|
||||
let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
|
||||
insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
MentionUri::PastedImage.name().into(),
|
||||
IconName::Image.path().into(),
|
||||
Some(Task::ready(Ok(image.clone())).shared()),
|
||||
editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
(*excerpt_id, text_anchor, multibuffer_anchor)
|
||||
});
|
||||
|
||||
let content_len = replacement_text.len();
|
||||
let Some(start_anchor) = multibuffer_anchor else {
|
||||
continue;
|
||||
};
|
||||
let end_anchor = self.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
|
||||
});
|
||||
let image = Arc::new(image);
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
MentionUri::PastedImage.name().into(),
|
||||
IconName::Image.path().into(),
|
||||
Some(Task::ready(Ok(image.clone())).shared()),
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let task = cx
|
||||
.spawn_in(window, {
|
||||
async move |_, cx| {
|
||||
)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let task = cx
|
||||
.spawn(async move |cx| {
|
||||
let format = image.format;
|
||||
let image = cx
|
||||
.update(|_, cx| LanguageModelImage::from_image(image, cx))
|
||||
@@ -994,15 +1035,16 @@ impl MessageEditor {
|
||||
} else {
|
||||
Err("Failed to convert image".into())
|
||||
}
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.mention_set
|
||||
.mentions
|
||||
.insert(crease_id, (MentionUri::PastedImage, task.clone()))
|
||||
})
|
||||
.shared();
|
||||
.ok();
|
||||
|
||||
self.mention_set
|
||||
.mentions
|
||||
.insert(crease_id, (MentionUri::PastedImage, task.clone()));
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if task.await.notify_async_err(cx).is_none() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
@@ -1012,9 +1054,9 @@ impl MessageEditor {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn insert_dragged_files(
|
||||
|
||||
@@ -251,17 +251,17 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.start_slot::<Icon>(model_info.icon.map(|icon| {
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pl_0p5()
|
||||
.gap_1p5()
|
||||
.w(px(240.))
|
||||
.when_some(model_info.icon, |this, icon| {
|
||||
this.child(
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small)
|
||||
)
|
||||
})
|
||||
.child(Label::new(model_info.name.clone()).truncate()),
|
||||
)
|
||||
.end_slot(div().pr_3().when(is_selected, |this| {
|
||||
|
||||
@@ -51,7 +51,7 @@ use ui::{
|
||||
PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use workspace::{CollaboratorId, NewTerminal, Workspace};
|
||||
use zed_actions::agent::{Chat, ToggleModelSelector};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
|
||||
@@ -69,8 +69,8 @@ use crate::ui::{
|
||||
};
|
||||
use crate::{
|
||||
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
|
||||
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, OpenHistory, RejectAll,
|
||||
RejectOnce, ToggleBurnMode, ToggleProfileSelector,
|
||||
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory,
|
||||
RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -278,6 +278,7 @@ pub struct AcpThreadView {
|
||||
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
|
||||
thread_retry_status: Option<RetryStatus>,
|
||||
thread_error: Option<ThreadError>,
|
||||
thread_error_markdown: Option<Entity<Markdown>>,
|
||||
thread_feedback: ThreadFeedbackState,
|
||||
list_state: ListState,
|
||||
auth_task: Option<Task<()>>,
|
||||
@@ -415,6 +416,7 @@ impl AcpThreadView {
|
||||
list_state: list_state,
|
||||
thread_retry_status: None,
|
||||
thread_error: None,
|
||||
thread_error_markdown: None,
|
||||
thread_feedback: Default::default(),
|
||||
auth_task: None,
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
@@ -798,6 +800,7 @@ impl AcpThreadView {
|
||||
|
||||
if should_retry {
|
||||
self.thread_error = None;
|
||||
self.thread_error_markdown = None;
|
||||
self.reset(window, cx);
|
||||
}
|
||||
}
|
||||
@@ -1327,6 +1330,7 @@ impl AcpThreadView {
|
||||
|
||||
fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
|
||||
self.thread_error = None;
|
||||
self.thread_error_markdown = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -3140,7 +3144,7 @@ impl AcpThreadView {
|
||||
.text_ui_sm(cx)
|
||||
.h_full()
|
||||
.children(terminal_view.map(|terminal_view| {
|
||||
if terminal_view
|
||||
let element = if terminal_view
|
||||
.read(cx)
|
||||
.content_mode(window, cx)
|
||||
.is_scrollable()
|
||||
@@ -3148,7 +3152,15 @@ impl AcpThreadView {
|
||||
div().h_72().child(terminal_view).into_any_element()
|
||||
} else {
|
||||
terminal_view.into_any_element()
|
||||
}
|
||||
};
|
||||
|
||||
div()
|
||||
.on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
|
||||
window.dispatch_action(NewThread.boxed_clone(), cx);
|
||||
cx.stop_propagation();
|
||||
}))
|
||||
.child(element)
|
||||
.into_any_element()
|
||||
})),
|
||||
)
|
||||
})
|
||||
@@ -5344,9 +5356,9 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_thread_error(&self, cx: &mut Context<Self>) -> Option<Div> {
|
||||
fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
let content = match self.thread_error.as_ref()? {
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), window, cx),
|
||||
ThreadError::Refusal => self.render_refusal_error(cx),
|
||||
ThreadError::AuthenticationRequired(error) => {
|
||||
self.render_authentication_required_error(error.clone(), cx)
|
||||
@@ -5431,7 +5443,12 @@ impl AcpThreadView {
|
||||
.dismiss_action(self.dismiss_error_button(cx))
|
||||
}
|
||||
|
||||
fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
|
||||
fn render_any_thread_error(
|
||||
&mut self,
|
||||
error: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) -> Callout {
|
||||
let can_resume = self
|
||||
.thread()
|
||||
.map_or(false, |thread| thread.read(cx).can_resume(cx));
|
||||
@@ -5444,11 +5461,24 @@ impl AcpThreadView {
|
||||
supports_burn_mode && thread.completion_mode() == CompletionMode::Normal
|
||||
});
|
||||
|
||||
let markdown = if let Some(markdown) = &self.thread_error_markdown {
|
||||
markdown.clone()
|
||||
} else {
|
||||
let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
|
||||
self.thread_error_markdown = Some(markdown.clone());
|
||||
markdown
|
||||
};
|
||||
|
||||
let markdown_style = default_markdown_style(false, true, window, cx);
|
||||
let description = self
|
||||
.render_markdown(markdown, markdown_style)
|
||||
.into_any_element();
|
||||
|
||||
Callout::new()
|
||||
.severity(Severity::Error)
|
||||
.title("Error")
|
||||
.icon(IconName::XCircle)
|
||||
.description(error.clone())
|
||||
.title("An Error Happened")
|
||||
.description_slot(description)
|
||||
.actions_slot(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
@@ -5467,11 +5497,9 @@ impl AcpThreadView {
|
||||
})
|
||||
.when(can_resume, |this| {
|
||||
this.child(
|
||||
Button::new("retry", "Retry")
|
||||
.icon(IconName::RotateCw)
|
||||
.icon_position(IconPosition::Start)
|
||||
IconButton::new("retry", IconName::RotateCw)
|
||||
.icon_size(IconSize::Small)
|
||||
.label_size(LabelSize::Small)
|
||||
.tooltip(Tooltip::text("Retry Generation"))
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.resume_chat(cx);
|
||||
})),
|
||||
@@ -5613,7 +5641,6 @@ impl AcpThreadView {
|
||||
|
||||
IconButton::new("copy", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Copy Error Message"))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
||||
@@ -5623,7 +5650,6 @@ impl AcpThreadView {
|
||||
fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss Error"))
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _, cx| {
|
||||
@@ -5841,7 +5867,7 @@ impl Render for AcpThreadView {
|
||||
None
|
||||
}
|
||||
})
|
||||
.children(self.render_thread_error(cx))
|
||||
.children(self.render_thread_error(window, cx))
|
||||
.when_some(
|
||||
self.new_server_version_available.as_ref().filter(|_| {
|
||||
!has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
|
||||
@@ -5907,7 +5933,6 @@ fn default_markdown_style(
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: colors.element_selection_background,
|
||||
code_block_overflow_x_scroll: true,
|
||||
table_overflow_x_scroll: true,
|
||||
heading_level_styles: Some(HeadingLevelStyles {
|
||||
h1: Some(TextStyleRefinement {
|
||||
font_size: Some(rems(1.15).into()),
|
||||
@@ -5975,6 +6000,7 @@ fn default_markdown_style(
|
||||
},
|
||||
link: TextStyleRefinement {
|
||||
background_color: Some(colors.editor_foreground.opacity(0.025)),
|
||||
color: Some(colors.text_accent),
|
||||
underline: Some(UnderlineStyle {
|
||||
color: Some(colors.text_accent.opacity(0.5)),
|
||||
thickness: px(1.),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mod add_llm_provider_modal;
|
||||
mod configure_context_server_modal;
|
||||
pub mod configure_context_server_modal;
|
||||
mod configure_context_server_tools_modal;
|
||||
mod manage_profiles_modal;
|
||||
mod tool_picker;
|
||||
@@ -46,9 +46,8 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||
pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
|
||||
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||
|
||||
use crate::{
|
||||
AddContextServer,
|
||||
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
||||
use crate::agent_configuration::add_llm_provider_modal::{
|
||||
AddLlmProviderModal, LlmCompatibleProvider,
|
||||
};
|
||||
|
||||
pub struct AgentConfiguration {
|
||||
@@ -553,7 +552,9 @@ impl AgentConfiguration {
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
menu.entry("Add Custom Server", None, {
|
||||
|window, cx| window.dispatch_action(AddContextServer.boxed_clone(), cx)
|
||||
|window, cx| {
|
||||
window.dispatch_action(crate::AddContextServer.boxed_clone(), cx)
|
||||
}
|
||||
})
|
||||
.entry("Install from Extensions", None, {
|
||||
|window, cx| {
|
||||
@@ -651,7 +652,7 @@ impl AgentConfiguration {
|
||||
let is_running = matches!(server_status, ContextServerStatus::Running);
|
||||
let item_id = SharedString::from(context_server_id.0.clone());
|
||||
// Servers without a configuration can only be provided by extensions.
|
||||
let provided_by_extension = server_configuration.is_none_or(|config| {
|
||||
let provided_by_extension = server_configuration.as_ref().is_none_or(|config| {
|
||||
matches!(
|
||||
config.as_ref(),
|
||||
ContextServerConfiguration::Extension { .. }
|
||||
@@ -707,7 +708,10 @@ impl AgentConfiguration {
|
||||
"Server is stopped.",
|
||||
),
|
||||
};
|
||||
|
||||
let is_remote = server_configuration
|
||||
.as_ref()
|
||||
.map(|config| matches!(config.as_ref(), ContextServerConfiguration::Http { .. }))
|
||||
.unwrap_or(false);
|
||||
let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("context-server-config-menu", IconName::Settings)
|
||||
@@ -730,14 +734,25 @@ impl AgentConfiguration {
|
||||
let language_registry = language_registry.clone();
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
ConfigureContextServerModal::show_modal_for_existing_server(
|
||||
context_server_id.clone(),
|
||||
language_registry.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
if is_remote {
|
||||
crate::agent_configuration::configure_context_server_modal::ConfigureContextServerModal::show_modal_for_existing_server(
|
||||
context_server_id.clone(),
|
||||
language_registry.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
} else {
|
||||
ConfigureContextServerModal::show_modal_for_existing_server(
|
||||
context_server_id.clone(),
|
||||
language_registry.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}).when(tool_count > 0, |this| this.entry("View Tools", None, {
|
||||
let context_server_id = context_server_id.clone();
|
||||
|
||||
@@ -3,16 +3,42 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
|
||||
use gpui::{
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, ScrollHandle, Task,
|
||||
};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities};
|
||||
use settings::{OpenAiCompatibleSettingsContent, update_settings_file};
|
||||
use ui::{
|
||||
Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
|
||||
Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState,
|
||||
WithScrollbar, prelude::*,
|
||||
};
|
||||
use ui_input::InputField;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
fn single_line_input(
|
||||
label: impl Into<SharedString>,
|
||||
placeholder: impl Into<SharedString>,
|
||||
text: Option<&str>,
|
||||
tab_index: isize,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<InputField> {
|
||||
cx.new(|cx| {
|
||||
let input = InputField::new(window, cx, placeholder)
|
||||
.label(label)
|
||||
.tab_index(tab_index)
|
||||
.tab_stop(true);
|
||||
|
||||
if let Some(text) = text {
|
||||
input
|
||||
.editor()
|
||||
.update(cx, |editor, cx| editor.set_text(text, window, cx));
|
||||
}
|
||||
input
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum LlmCompatibleProvider {
|
||||
OpenAi,
|
||||
@@ -41,12 +67,14 @@ struct AddLlmProviderInput {
|
||||
|
||||
impl AddLlmProviderInput {
|
||||
fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self {
|
||||
let provider_name = single_line_input("Provider Name", provider.name(), None, window, cx);
|
||||
let api_url = single_line_input("API URL", provider.api_url(), None, window, cx);
|
||||
let provider_name =
|
||||
single_line_input("Provider Name", provider.name(), None, 1, window, cx);
|
||||
let api_url = single_line_input("API URL", provider.api_url(), None, 2, window, cx);
|
||||
let api_key = single_line_input(
|
||||
"API Key",
|
||||
"000000000000000000000000000000000000000000000000",
|
||||
None,
|
||||
3,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -55,12 +83,13 @@ impl AddLlmProviderInput {
|
||||
provider_name,
|
||||
api_url,
|
||||
api_key,
|
||||
models: vec![ModelInput::new(window, cx)],
|
||||
models: vec![ModelInput::new(0, window, cx)],
|
||||
}
|
||||
}
|
||||
|
||||
fn add_model(&mut self, window: &mut Window, cx: &mut App) {
|
||||
self.models.push(ModelInput::new(window, cx));
|
||||
let model_index = self.models.len();
|
||||
self.models.push(ModelInput::new(model_index, window, cx));
|
||||
}
|
||||
|
||||
fn remove_model(&mut self, index: usize) {
|
||||
@@ -84,11 +113,14 @@ struct ModelInput {
|
||||
}
|
||||
|
||||
impl ModelInput {
|
||||
fn new(window: &mut Window, cx: &mut App) -> Self {
|
||||
fn new(model_index: usize, window: &mut Window, cx: &mut App) -> Self {
|
||||
let base_tab_index = (3 + (model_index * 4)) as isize;
|
||||
|
||||
let model_name = single_line_input(
|
||||
"Model Name",
|
||||
"e.g. gpt-4o, claude-opus-4, gemini-2.5-pro",
|
||||
None,
|
||||
base_tab_index + 1,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -96,6 +128,7 @@ impl ModelInput {
|
||||
"Max Completion Tokens",
|
||||
"200000",
|
||||
Some("200000"),
|
||||
base_tab_index + 2,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -103,16 +136,26 @@ impl ModelInput {
|
||||
"Max Output Tokens",
|
||||
"Max Output Tokens",
|
||||
Some("32000"),
|
||||
base_tab_index + 3,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
|
||||
let max_tokens = single_line_input(
|
||||
"Max Tokens",
|
||||
"Max Tokens",
|
||||
Some("200000"),
|
||||
base_tab_index + 4,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let ModelCapabilities {
|
||||
tools,
|
||||
images,
|
||||
parallel_tool_calls,
|
||||
prompt_cache_key,
|
||||
} = ModelCapabilities::default();
|
||||
|
||||
Self {
|
||||
name: model_name,
|
||||
max_completion_tokens,
|
||||
@@ -165,24 +208,6 @@ impl ModelInput {
|
||||
}
|
||||
}
|
||||
|
||||
fn single_line_input(
|
||||
label: impl Into<SharedString>,
|
||||
placeholder: impl Into<SharedString>,
|
||||
text: Option<&str>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<InputField> {
|
||||
cx.new(|cx| {
|
||||
let input = InputField::new(window, cx, placeholder).label(label);
|
||||
if let Some(text) = text {
|
||||
input
|
||||
.editor()
|
||||
.update(cx, |editor, cx| editor.set_text(text, window, cx));
|
||||
}
|
||||
input
|
||||
})
|
||||
}
|
||||
|
||||
fn save_provider_to_settings(
|
||||
input: &AddLlmProviderInput,
|
||||
cx: &mut App,
|
||||
@@ -258,6 +283,7 @@ fn save_provider_to_settings(
|
||||
pub struct AddLlmProviderModal {
|
||||
provider: LlmCompatibleProvider,
|
||||
input: AddLlmProviderInput,
|
||||
scroll_handle: ScrollHandle,
|
||||
focus_handle: FocusHandle,
|
||||
last_error: Option<SharedString>,
|
||||
}
|
||||
@@ -278,6 +304,7 @@ impl AddLlmProviderModal {
|
||||
provider,
|
||||
last_error: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,6 +445,19 @@ impl AddLlmProviderModal {
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_next();
|
||||
}
|
||||
|
||||
fn on_tab_prev(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
window.focus_prev();
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for AddLlmProviderModal {}
|
||||
@@ -431,15 +471,27 @@ impl Focusable for AddLlmProviderModal {
|
||||
impl ModalView for AddLlmProviderModal {}
|
||||
|
||||
impl Render for AddLlmProviderModal {
|
||||
fn render(&mut self, _window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
div()
|
||||
let window_size = window.viewport_size();
|
||||
let rem_size = window.rem_size();
|
||||
let is_large_window = window_size.height / rem_size > rems_from_px(600.).0;
|
||||
|
||||
let modal_max_height = if is_large_window {
|
||||
rems_from_px(450.)
|
||||
} else {
|
||||
rems_from_px(200.)
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.id("add-llm-provider-modal")
|
||||
.key_context("AddLlmProviderModal")
|
||||
.w(rems(34.))
|
||||
.elevation_3(cx)
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::on_tab))
|
||||
.on_action(cx.listener(Self::on_tab_prev))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
@@ -462,17 +514,25 @@ impl Render for AddLlmProviderModal {
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.id("modal_content")
|
||||
div()
|
||||
.size_full()
|
||||
.max_h_128()
|
||||
.overflow_y_scroll()
|
||||
.px(DynamicSpacing::Base12.rems(cx))
|
||||
.gap(DynamicSpacing::Base04.rems(cx))
|
||||
.child(self.input.provider_name.clone())
|
||||
.child(self.input.api_url.clone())
|
||||
.child(self.input.api_key.clone())
|
||||
.child(self.render_model_section(cx)),
|
||||
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
|
||||
.child(
|
||||
v_flex()
|
||||
.id("modal_content")
|
||||
.size_full()
|
||||
.tab_group()
|
||||
.max_h(modal_max_height)
|
||||
.pl_3()
|
||||
.pr_4()
|
||||
.gap_2()
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.child(self.input.provider_name.clone())
|
||||
.child(self.input.api_url.clone())
|
||||
.child(self.input.api_key.clone())
|
||||
.child(self.render_model_section(cx)),
|
||||
),
|
||||
)
|
||||
.footer(
|
||||
ModalFooter::new().end_slot(
|
||||
@@ -642,7 +702,7 @@ mod tests {
|
||||
let cx = setup_test(cx).await;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
let model_input = ModelInput::new(window, cx);
|
||||
let model_input = ModelInput::new(0, window, cx);
|
||||
model_input.name.update(cx, |input, cx| {
|
||||
input.editor().update(cx, |editor, cx| {
|
||||
editor.set_text("somemodel", window, cx);
|
||||
@@ -678,7 +738,7 @@ mod tests {
|
||||
let cx = setup_test(cx).await;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
let mut model_input = ModelInput::new(window, cx);
|
||||
let mut model_input = ModelInput::new(0, window, cx);
|
||||
model_input.name.update(cx, |input, cx| {
|
||||
input.editor().update(cx, |editor, cx| {
|
||||
editor.set_text("somemodel", window, cx);
|
||||
@@ -703,7 +763,7 @@ mod tests {
|
||||
let cx = setup_test(cx).await;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
let mut model_input = ModelInput::new(window, cx);
|
||||
let mut model_input = ModelInput::new(0, window, cx);
|
||||
model_input.name.update(cx, |input, cx| {
|
||||
input.editor().update(cx, |editor, cx| {
|
||||
editor.set_text("somemodel", window, cx);
|
||||
@@ -767,7 +827,7 @@ mod tests {
|
||||
models.iter().enumerate()
|
||||
{
|
||||
if i >= input.models.len() {
|
||||
input.models.push(ModelInput::new(window, cx));
|
||||
input.models.push(ModelInput::new(i, window, cx));
|
||||
}
|
||||
let model = &mut input.models[i];
|
||||
set_text(&model.name, name, window, cx);
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use context_server::{ContextServerCommand, ContextServerId};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
@@ -20,6 +21,7 @@ use project::{
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
@@ -37,6 +39,11 @@ enum ConfigurationTarget {
|
||||
id: ContextServerId,
|
||||
command: ContextServerCommand,
|
||||
},
|
||||
ExistingHttp {
|
||||
id: ContextServerId,
|
||||
url: String,
|
||||
headers: HashMap<String, String>,
|
||||
},
|
||||
Extension {
|
||||
id: ContextServerId,
|
||||
repository_url: Option<SharedString>,
|
||||
@@ -47,9 +54,11 @@ enum ConfigurationTarget {
|
||||
enum ConfigurationSource {
|
||||
New {
|
||||
editor: Entity<Editor>,
|
||||
is_http: bool,
|
||||
},
|
||||
Existing {
|
||||
editor: Entity<Editor>,
|
||||
is_http: bool,
|
||||
},
|
||||
Extension {
|
||||
id: ContextServerId,
|
||||
@@ -97,6 +106,7 @@ impl ConfigurationSource {
|
||||
match target {
|
||||
ConfigurationTarget::New => ConfigurationSource::New {
|
||||
editor: create_editor(context_server_input(None), jsonc_language, window, cx),
|
||||
is_http: false,
|
||||
},
|
||||
ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
|
||||
editor: create_editor(
|
||||
@@ -105,6 +115,20 @@ impl ConfigurationSource {
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
is_http: false,
|
||||
},
|
||||
ConfigurationTarget::ExistingHttp {
|
||||
id,
|
||||
url,
|
||||
headers: auth,
|
||||
} => ConfigurationSource::Existing {
|
||||
editor: create_editor(
|
||||
context_server_http_input(Some((id, url, auth))),
|
||||
jsonc_language,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
is_http: true,
|
||||
},
|
||||
ConfigurationTarget::Extension {
|
||||
id,
|
||||
@@ -141,16 +165,30 @@ impl ConfigurationSource {
|
||||
|
||||
fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
|
||||
match self {
|
||||
ConfigurationSource::New { editor } | ConfigurationSource::Existing { editor } => {
|
||||
parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
|
||||
(
|
||||
id,
|
||||
ContextServerSettings::Custom {
|
||||
enabled: true,
|
||||
command,
|
||||
},
|
||||
)
|
||||
})
|
||||
ConfigurationSource::New { editor, is_http }
|
||||
| ConfigurationSource::Existing { editor, is_http } => {
|
||||
if *is_http {
|
||||
parse_http_input(&editor.read(cx).text(cx)).map(|(id, url, auth)| {
|
||||
(
|
||||
id,
|
||||
ContextServerSettings::Http {
|
||||
enabled: true,
|
||||
url,
|
||||
headers: auth,
|
||||
},
|
||||
)
|
||||
})
|
||||
} else {
|
||||
parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
|
||||
(
|
||||
id,
|
||||
ContextServerSettings::Custom {
|
||||
enabled: true,
|
||||
command,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
ConfigurationSource::Extension {
|
||||
id,
|
||||
@@ -212,6 +250,66 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
|
||||
)
|
||||
}
|
||||
|
||||
fn context_server_http_input(
|
||||
existing: Option<(ContextServerId, String, HashMap<String, String>)>,
|
||||
) -> String {
|
||||
let (name, url, headers) = match existing {
|
||||
Some((id, url, headers)) => {
|
||||
let header = if headers.is_empty() {
|
||||
r#"// "Authorization": "Bearer <token>"#.to_string()
|
||||
} else {
|
||||
let json = serde_json::to_string_pretty(&headers).unwrap();
|
||||
let mut lines = json.split("\n").collect::<Vec<_>>();
|
||||
if lines.len() > 1 {
|
||||
lines.remove(0);
|
||||
lines.pop();
|
||||
}
|
||||
lines
|
||||
.into_iter()
|
||||
.map(|line| format!(" {}", line))
|
||||
.collect::<String>()
|
||||
};
|
||||
(id.0.to_string(), url, header)
|
||||
}
|
||||
None => (
|
||||
"some-remote-server".to_string(),
|
||||
"https://example.com/mcp".to_string(),
|
||||
r#"// "Authorization": "Bearer <token>"#.to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
format!(
|
||||
r#"{{
|
||||
/// The name of your remote MCP server
|
||||
"{name}": {{
|
||||
/// The URL of the remote MCP server
|
||||
"url": "{url}",
|
||||
"headers": {{
|
||||
/// Any headers to send along
|
||||
{headers}
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_http_input(text: &str) -> Result<(ContextServerId, String, HashMap<String, String>)> {
|
||||
#[derive(Deserialize)]
|
||||
struct Temp {
|
||||
url: String,
|
||||
#[serde(default)]
|
||||
headers: HashMap<String, String>,
|
||||
}
|
||||
let value: HashMap<String, Temp> = serde_json_lenient::from_str(text)?;
|
||||
if value.len() != 1 {
|
||||
anyhow::bail!("Expected exactly one context server configuration");
|
||||
}
|
||||
|
||||
let (key, value) = value.into_iter().next().unwrap();
|
||||
|
||||
Ok((ContextServerId(key.into()), value.url, value.headers))
|
||||
}
|
||||
|
||||
fn resolve_context_server_extension(
|
||||
id: ContextServerId,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
@@ -312,6 +410,15 @@ impl ConfigureContextServerModal {
|
||||
id: server_id,
|
||||
command,
|
||||
}),
|
||||
ContextServerSettings::Http {
|
||||
enabled: _,
|
||||
url,
|
||||
headers,
|
||||
} => Some(ConfigurationTarget::ExistingHttp {
|
||||
id: server_id,
|
||||
url,
|
||||
headers,
|
||||
}),
|
||||
ContextServerSettings::Extension { .. } => {
|
||||
match workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
@@ -353,6 +460,7 @@ impl ConfigureContextServerModal {
|
||||
state: State::Idle,
|
||||
original_server_id: match &target {
|
||||
ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
|
||||
ConfigurationTarget::ExistingHttp { id, .. } => Some(id.clone()),
|
||||
ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
|
||||
ConfigurationTarget::New => None,
|
||||
},
|
||||
@@ -481,7 +589,7 @@ impl ModalView for ConfigureContextServerModal {}
|
||||
impl Focusable for ConfigureContextServerModal {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match &self.source {
|
||||
ConfigurationSource::New { editor } => editor.focus_handle(cx),
|
||||
ConfigurationSource::New { editor, .. } => editor.focus_handle(cx),
|
||||
ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx),
|
||||
ConfigurationSource::Extension { editor, .. } => editor
|
||||
.as_ref()
|
||||
@@ -527,9 +635,10 @@ impl ConfigureContextServerModal {
|
||||
}
|
||||
|
||||
fn render_modal_content(&self, cx: &App) -> AnyElement {
|
||||
// All variants now use single editor approach
|
||||
let editor = match &self.source {
|
||||
ConfigurationSource::New { editor } => editor,
|
||||
ConfigurationSource::Existing { editor } => editor,
|
||||
ConfigurationSource::New { editor, .. } => editor,
|
||||
ConfigurationSource::Existing { editor, .. } => editor,
|
||||
ConfigurationSource::Extension { editor, .. } => {
|
||||
let Some(editor) = editor else {
|
||||
return div().into_any_element();
|
||||
@@ -601,6 +710,36 @@ impl ConfigureContextServerModal {
|
||||
move |_, _, cx| cx.open_url(&repository_url)
|
||||
}),
|
||||
)
|
||||
} else if let ConfigurationSource::New { is_http, .. } = &self.source {
|
||||
let label = if *is_http {
|
||||
"Run command"
|
||||
} else {
|
||||
"Connect via HTTP"
|
||||
};
|
||||
let tooltip = if *is_http {
|
||||
"Configure an MCP serevr that runs on stdin/stdout."
|
||||
} else {
|
||||
"Configure an MCP server that you connect to over HTTP"
|
||||
};
|
||||
|
||||
Some(
|
||||
Button::new("toggle-kind", label)
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.on_click(cx.listener(|this, _, window, cx| match &mut this.source {
|
||||
ConfigurationSource::New { editor, is_http } => {
|
||||
*is_http = !*is_http;
|
||||
let new_text = if *is_http {
|
||||
context_server_http_input(None)
|
||||
} else {
|
||||
context_server_input(None)
|
||||
};
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_text(new_text, window, cx);
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
||||
@@ -1892,6 +1892,9 @@ impl AgentPanel {
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.new_thread_menu_handle.clone())
|
||||
.menu({
|
||||
let selected_agent = self.selected_agent.clone();
|
||||
let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let is_via_collab = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
@@ -1905,7 +1908,6 @@ impl AgentPanel {
|
||||
let active_thread = active_thread.clone();
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, cx| {
|
||||
menu.context(focus_handle.clone())
|
||||
.header("Zed Agent")
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
|
||||
@@ -1929,9 +1931,11 @@ impl AgentPanel {
|
||||
}
|
||||
})
|
||||
.item(
|
||||
ContextMenuEntry::new("New Thread")
|
||||
.action(NewThread.boxed_clone())
|
||||
.icon(IconName::Thread)
|
||||
ContextMenuEntry::new("Zed Agent")
|
||||
.when(is_agent_selected(AgentType::NativeAgent) | is_agent_selected(AgentType::TextThread) , |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
.icon(IconName::ZedAgent)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
@@ -1955,10 +1959,10 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Text Thread")
|
||||
ContextMenuEntry::new("Text Thread")
|
||||
.action(NewTextThread.boxed_clone())
|
||||
.icon(IconName::TextThread)
|
||||
.icon_color(Color::Muted)
|
||||
.action(NewTextThread.boxed_clone())
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
@@ -1983,7 +1987,10 @@ impl AgentPanel {
|
||||
.separator()
|
||||
.header("External Agents")
|
||||
.item(
|
||||
ContextMenuEntry::new("New Claude Code")
|
||||
ContextMenuEntry::new("Claude Code")
|
||||
.when(is_agent_selected(AgentType::ClaudeCode), |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
.icon(IconName::AiClaude)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -2009,7 +2016,10 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Codex CLI")
|
||||
ContextMenuEntry::new("Codex CLI")
|
||||
.when(is_agent_selected(AgentType::Codex), |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
.icon(IconName::AiOpenAi)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -2035,7 +2045,10 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Gemini CLI")
|
||||
ContextMenuEntry::new("Gemini CLI")
|
||||
.when(is_agent_selected(AgentType::Gemini), |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
.icon(IconName::AiGemini)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_via_collab)
|
||||
@@ -2061,8 +2074,8 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.map(|mut menu| {
|
||||
let agent_server_store_read = agent_server_store.read(cx);
|
||||
let agent_names = agent_server_store_read
|
||||
let agent_server_store = agent_server_store.read(cx);
|
||||
let agent_names = agent_server_store
|
||||
.external_agents()
|
||||
.filter(|name| {
|
||||
name.0 != GEMINI_NAME
|
||||
@@ -2071,21 +2084,38 @@ impl AgentPanel {
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let custom_settings = cx
|
||||
.global::<SettingsStore>()
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.clone();
|
||||
|
||||
for agent_name in agent_names {
|
||||
let icon_path = agent_server_store_read.agent_icon(&agent_name);
|
||||
let mut entry =
|
||||
ContextMenuEntry::new(format!("New {}", agent_name));
|
||||
let icon_path = agent_server_store.agent_icon(&agent_name);
|
||||
|
||||
let mut entry = ContextMenuEntry::new(agent_name.clone());
|
||||
|
||||
let command = custom_settings
|
||||
.get(&agent_name.0)
|
||||
.map(|settings| settings.command.clone())
|
||||
.unwrap_or(placeholder_command());
|
||||
|
||||
if let Some(icon_path) = icon_path {
|
||||
entry = entry.custom_icon_svg(icon_path);
|
||||
} else {
|
||||
entry = entry.icon(IconName::Terminal);
|
||||
}
|
||||
entry = entry
|
||||
.when(
|
||||
is_agent_selected(AgentType::Custom {
|
||||
name: agent_name.0.clone(),
|
||||
command: command.clone(),
|
||||
}),
|
||||
|this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_via_collab)
|
||||
.handler({
|
||||
@@ -2125,6 +2155,7 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
menu = menu.item(entry);
|
||||
}
|
||||
|
||||
@@ -2157,7 +2188,7 @@ impl AgentPanel {
|
||||
.id("selected_agent_icon")
|
||||
.when_some(selected_agent_custom_icon, |this, icon_path| {
|
||||
let label = selected_agent_label.clone();
|
||||
this.px(DynamicSpacing::Base02.rems(cx))
|
||||
this.px_1()
|
||||
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
|
||||
@@ -2166,7 +2197,7 @@ impl AgentPanel {
|
||||
.when(!has_custom_icon, |this| {
|
||||
this.when_some(self.selected_agent.icon(), |this, icon| {
|
||||
let label = selected_agent_label.clone();
|
||||
this.px(DynamicSpacing::Base02.rems(cx))
|
||||
this.px_1()
|
||||
.child(Icon::new(icon).color(Color::Muted))
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
|
||||
|
||||
@@ -346,7 +346,9 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
filter.show_namespace("supermaven");
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
}
|
||||
EditPredictionProvider::Zed | EditPredictionProvider::Codestral => {
|
||||
EditPredictionProvider::Zed
|
||||
| EditPredictionProvider::Codestral
|
||||
| EditPredictionProvider::Experimental(_) => {
|
||||
filter.show_namespace("edit_prediction");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
|
||||
@@ -492,17 +492,15 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.start_slot(
|
||||
Icon::new(model_info.icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pl_0p5()
|
||||
.gap_1p5()
|
||||
.w(px(240.))
|
||||
.child(
|
||||
Icon::new(model_info.icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(Label::new(model_info.model.name().0).truncate()),
|
||||
)
|
||||
.end_slot(div().pr_3().when(is_selected, |this| {
|
||||
|
||||
@@ -1679,7 +1679,7 @@ impl TextThreadEditor {
|
||||
) {
|
||||
cx.stop_propagation();
|
||||
|
||||
let images = if let Some(item) = cx.read_from_clipboard() {
|
||||
let mut images = if let Some(item) = cx.read_from_clipboard() {
|
||||
item.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::Image(image) = entry {
|
||||
@@ -1693,6 +1693,40 @@ impl TextThreadEditor {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if let Some(paths) = cx.read_from_clipboard() {
|
||||
for path in paths
|
||||
.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::ExternalPaths(paths) = entry {
|
||||
Some(paths.paths().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
{
|
||||
let Ok(content) = std::fs::read(path) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(format) = image::guess_format(&content) else {
|
||||
continue;
|
||||
};
|
||||
images.push(gpui::Image::from_bytes(
|
||||
match format {
|
||||
image::ImageFormat::Png => gpui::ImageFormat::Png,
|
||||
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
|
||||
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
|
||||
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
|
||||
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
|
||||
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
|
||||
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
|
||||
_ => continue,
|
||||
},
|
||||
content,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = if let Some(item) = cx.read_from_clipboard() {
|
||||
item.entries().first().and_then(|entry| {
|
||||
if let ClipboardEntry::String(text) = entry {
|
||||
@@ -2592,12 +2626,11 @@ impl SearchableItem for TextThreadEditor {
|
||||
&mut self,
|
||||
index: usize,
|
||||
matches: &[Self::Match],
|
||||
collapse: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.activate_match(index, matches, collapse, window, cx);
|
||||
editor.activate_match(index, matches, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ use paths::remote_servers_dir;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{RegisterSetting, Settings, SettingsStore};
|
||||
use smol::fs::File;
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
use smol::{fs::File, process::Command};
|
||||
use std::mem;
|
||||
use std::{
|
||||
env::{
|
||||
@@ -23,6 +23,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::command::new_smol_command;
|
||||
use workspace::Workspace;
|
||||
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
@@ -121,7 +122,7 @@ impl Drop for MacOsUnmounter<'_> {
|
||||
let mount_path = mem::take(&mut self.mount_path);
|
||||
self.background_executor
|
||||
.spawn(async move {
|
||||
let unmount_output = Command::new("hdiutil")
|
||||
let unmount_output = new_smol_command("hdiutil")
|
||||
.args(["detach", "-force"])
|
||||
.arg(&mount_path)
|
||||
.output()
|
||||
@@ -799,7 +800,7 @@ async fn install_release_linux(
|
||||
.await
|
||||
.context("failed to create directory into which to extract update")?;
|
||||
|
||||
let output = Command::new("tar")
|
||||
let output = new_smol_command("tar")
|
||||
.arg("-xzf")
|
||||
.arg(&downloaded_tar_gz)
|
||||
.arg("-C")
|
||||
@@ -834,7 +835,7 @@ async fn install_release_linux(
|
||||
to = PathBuf::from(prefix);
|
||||
}
|
||||
|
||||
let output = Command::new("rsync")
|
||||
let output = new_smol_command("rsync")
|
||||
.args(["-av", "--delete"])
|
||||
.arg(&from)
|
||||
.arg(&to)
|
||||
@@ -866,7 +867,7 @@ async fn install_release_macos(
|
||||
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
|
||||
|
||||
mounted_app_path.push("/");
|
||||
let output = Command::new("hdiutil")
|
||||
let output = new_smol_command("hdiutil")
|
||||
.args(["attach", "-nobrowse"])
|
||||
.arg(&downloaded_dmg)
|
||||
.arg("-mountroot")
|
||||
@@ -886,7 +887,7 @@ async fn install_release_macos(
|
||||
background_executor: cx.background_executor(),
|
||||
};
|
||||
|
||||
let output = Command::new("rsync")
|
||||
let output = new_smol_command("rsync")
|
||||
.args(["-av", "--delete"])
|
||||
.arg(&mounted_app_path)
|
||||
.arg(&running_app_path)
|
||||
@@ -917,7 +918,7 @@ async fn cleanup_windows() -> Result<()> {
|
||||
}
|
||||
|
||||
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
|
||||
let output = Command::new(downloaded_installer)
|
||||
let output = new_smol_command(downloaded_installer)
|
||||
.arg("/verysilent")
|
||||
.arg("/update=true")
|
||||
.arg("!desktopicon")
|
||||
|
||||
@@ -293,10 +293,11 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn metrics_enabled(self: &Arc<Self>) -> bool {
|
||||
let state = self.state.lock();
|
||||
let enabled = state.settings.metrics;
|
||||
drop(state);
|
||||
enabled
|
||||
self.state.lock().settings.metrics
|
||||
}
|
||||
|
||||
pub fn diagnostics_enabled(self: &Arc<Self>) -> bool {
|
||||
self.state.lock().settings.diagnostics
|
||||
}
|
||||
|
||||
pub fn set_authenticated_user_info(
|
||||
|
||||
@@ -267,6 +267,7 @@ impl UserStore {
|
||||
Status::SignedOut => {
|
||||
current_user_tx.send(None).await.ok();
|
||||
this.update(cx, |this, cx| {
|
||||
this.clear_plan_and_usage();
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
cx.notify();
|
||||
this.clear_contacts()
|
||||
@@ -779,6 +780,12 @@ impl UserStore {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear_plan_and_usage(&mut self) {
|
||||
self.plan_info = None;
|
||||
self.model_request_usage = None;
|
||||
self.edit_prediction_usage = None;
|
||||
}
|
||||
|
||||
fn update_authenticated_user(
|
||||
&mut self,
|
||||
response: GetAuthenticatedUserResponse,
|
||||
|
||||
@@ -76,6 +76,10 @@ pub enum PromptFormat {
|
||||
OldTextNewText,
|
||||
/// Prompt format intended for use via zeta_cli
|
||||
OnlySnippets,
|
||||
/// One-sentence instructions used in fine-tuned models
|
||||
Minimal,
|
||||
/// One-sentence instructions + FIM-like template
|
||||
MinimalQwen,
|
||||
}
|
||||
|
||||
impl PromptFormat {
|
||||
@@ -102,6 +106,8 @@ impl std::fmt::Display for PromptFormat {
|
||||
PromptFormat::OnlySnippets => write!(f, "Only Snippets"),
|
||||
PromptFormat::NumLinesUniDiff => write!(f, "Numbered Lines / Unified Diff"),
|
||||
PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
|
||||
PromptFormat::Minimal => write!(f, "Minimal"),
|
||||
PromptFormat::MinimalQwen => write!(f, "Minimal + Qwen FIM"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ pub mod retrieval_prompt;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use cloud_llm_client::predict_edits_v3::{
|
||||
self, DiffPathFmt, Excerpt, Line, Point, PromptFormat, ReferencedDeclaration,
|
||||
self, DiffPathFmt, Event, Excerpt, IncludedFile, Line, Point, PromptFormat,
|
||||
ReferencedDeclaration,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use ordered_float::OrderedFloat;
|
||||
@@ -31,7 +32,7 @@ const MARKED_EXCERPT_INSTRUCTIONS: &str = indoc! {"
|
||||
|
||||
Other code is provided for context, and `…` indicates when code has been skipped.
|
||||
|
||||
# Edit History:
|
||||
## Edit History
|
||||
|
||||
"};
|
||||
|
||||
@@ -49,7 +50,7 @@ const LABELED_SECTIONS_INSTRUCTIONS: &str = indoc! {r#"
|
||||
println!("{i}");
|
||||
}
|
||||
|
||||
# Edit History:
|
||||
## Edit History
|
||||
|
||||
"#};
|
||||
|
||||
@@ -86,6 +87,13 @@ const NUMBERED_LINES_INSTRUCTIONS: &str = indoc! {r#"
|
||||
|
||||
"#};
|
||||
|
||||
const STUDENT_MODEL_INSTRUCTIONS: &str = indoc! {r#"
|
||||
You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.
|
||||
|
||||
## Edit History
|
||||
|
||||
"#};
|
||||
|
||||
const UNIFIED_DIFF_REMINDER: &str = indoc! {"
|
||||
---
|
||||
|
||||
@@ -100,18 +108,27 @@ const UNIFIED_DIFF_REMINDER: &str = indoc! {"
|
||||
to uniquely identify it amongst all excerpts of code provided.
|
||||
"};
|
||||
|
||||
const MINIMAL_PROMPT_REMINDER: &str = indoc! {"
|
||||
---
|
||||
|
||||
Please analyze the edit history and the files, then provide the unified diff for your predicted edits.
|
||||
Do not include the cursor marker in your output.
|
||||
If you're editing multiple files, be sure to reflect filename in the hunk's header.
|
||||
"};
|
||||
|
||||
const XML_TAGS_INSTRUCTIONS: &str = indoc! {r#"
|
||||
# Instructions
|
||||
|
||||
You are an edit prediction agent in a code editor.
|
||||
Your job is to predict the next edit that the user will make,
|
||||
based on their last few edits and their current cursor location.
|
||||
|
||||
# Output Format
|
||||
Analyze the history of edits made by the user in order to infer what they are currently trying to accomplish.
|
||||
Then complete the remainder of the current change if it is incomplete, or predict the next edit the user intends to make.
|
||||
Always continue along the user's current trajectory, rather than changing course.
|
||||
|
||||
You must briefly explain your understanding of the user's goal, in one
|
||||
or two sentences, and then specify their next edit, using the following
|
||||
XML format:
|
||||
## Output Format
|
||||
|
||||
You should briefly explain your understanding of the user's overall goal in one sentence, then explain what the next change
|
||||
along the users current trajectory will be in another, and finally specify the next edit using the following XML-like format:
|
||||
|
||||
<edits path="my-project/src/myapp/cli.py">
|
||||
<old_text>
|
||||
@@ -137,20 +154,34 @@ const XML_TAGS_INSTRUCTIONS: &str = indoc! {r#"
|
||||
- Always close all tags properly
|
||||
- Don't include the <|user_cursor|> marker in your output.
|
||||
|
||||
# Edit History:
|
||||
## Edit History
|
||||
|
||||
"#};
|
||||
|
||||
const OLD_TEXT_NEW_TEXT_REMINDER: &str = indoc! {r#"
|
||||
---
|
||||
|
||||
Remember that the edits in the edit history have already been deployed.
|
||||
The files are currently as shown in the Code Excerpts section.
|
||||
Remember that the edits in the edit history have already been applied.
|
||||
"#};
|
||||
|
||||
pub fn build_prompt(
|
||||
request: &predict_edits_v3::PredictEditsRequest,
|
||||
) -> Result<(String, SectionLabels)> {
|
||||
let mut section_labels = Default::default();
|
||||
|
||||
match request.prompt_format {
|
||||
PromptFormat::MinimalQwen => {
|
||||
let prompt = MinimalQwenPrompt {
|
||||
events: request.events.clone(),
|
||||
cursor_point: request.cursor_point,
|
||||
cursor_path: request.excerpt_path.clone(),
|
||||
included_files: request.included_files.clone(),
|
||||
};
|
||||
return Ok((prompt.render(), section_labels));
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
let mut insertions = match request.prompt_format {
|
||||
PromptFormat::MarkedExcerpt => vec![
|
||||
(
|
||||
@@ -171,10 +202,12 @@ pub fn build_prompt(
|
||||
],
|
||||
PromptFormat::LabeledSections
|
||||
| PromptFormat::NumLinesUniDiff
|
||||
| PromptFormat::Minimal
|
||||
| PromptFormat::OldTextNewText => {
|
||||
vec![(request.cursor_point, CURSOR_MARKER)]
|
||||
}
|
||||
PromptFormat::OnlySnippets => vec![],
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
};
|
||||
|
||||
let mut prompt = match request.prompt_format {
|
||||
@@ -183,32 +216,59 @@ pub fn build_prompt(
|
||||
PromptFormat::NumLinesUniDiff => NUMBERED_LINES_INSTRUCTIONS.to_string(),
|
||||
PromptFormat::OldTextNewText => XML_TAGS_INSTRUCTIONS.to_string(),
|
||||
PromptFormat::OnlySnippets => String::new(),
|
||||
PromptFormat::Minimal => STUDENT_MODEL_INSTRUCTIONS.to_string(),
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
};
|
||||
|
||||
if request.events.is_empty() {
|
||||
prompt.push_str("(No edit history)\n\n");
|
||||
} else {
|
||||
prompt.push_str("Here are the latest edits made by the user, from earlier to later.\n\n");
|
||||
let edit_preamble = if request.prompt_format == PromptFormat::Minimal {
|
||||
"The following are the latest edits made by the user, from earlier to later.\n\n"
|
||||
} else {
|
||||
"Here are the latest edits made by the user, from earlier to later.\n\n"
|
||||
};
|
||||
prompt.push_str(edit_preamble);
|
||||
push_events(&mut prompt, &request.events);
|
||||
}
|
||||
|
||||
prompt.push_str(indoc! {"
|
||||
# Code Excerpts
|
||||
let excerpts_preamble = match request.prompt_format {
|
||||
PromptFormat::Minimal => indoc! {"
|
||||
## Part of the file under the cursor
|
||||
|
||||
The cursor marker <|user_cursor|> indicates the current user cursor position.
|
||||
The file is in current state, edits from edit history have been applied.
|
||||
"});
|
||||
(The cursor marker <|user_cursor|> indicates the current user cursor position.
|
||||
The file is in current state, edits from edit history has been applied.
|
||||
We only show part of the file around the cursor.
|
||||
You can only edit exactly this part of the file.
|
||||
We prepend line numbers (e.g., `123|<actual line>`); they are not part of the file.)
|
||||
"},
|
||||
PromptFormat::NumLinesUniDiff | PromptFormat::OldTextNewText => indoc! {"
|
||||
## Code Excerpts
|
||||
|
||||
if request.prompt_format == PromptFormat::NumLinesUniDiff {
|
||||
prompt.push_str(indoc! {"
|
||||
We prepend line numbers (e.g., `123|<actual line>`); they are not part of the file.
|
||||
"});
|
||||
}
|
||||
Here is some excerpts of code that you should take into account to predict the next edit.
|
||||
|
||||
The cursor position is marked by `<|user_cursor|>` as it stands after the last edit in the history.
|
||||
|
||||
In addition other excerpts are included to better understand what the edit will be, including the declaration
|
||||
or references of symbols around the cursor, or other similar code snippets that may need to be updated
|
||||
following patterns that appear in the edit history.
|
||||
|
||||
Consider each of them carefully in relation to the edit history, and that the user may not have navigated
|
||||
to the next place they want to edit yet.
|
||||
|
||||
Lines starting with `…` indicate omitted line ranges. These may appear inside multi-line code constructs.
|
||||
"},
|
||||
_ => indoc! {"
|
||||
## Code Excerpts
|
||||
|
||||
The cursor marker <|user_cursor|> indicates the current user cursor position.
|
||||
The file is in current state, edits from edit history have been applied.
|
||||
"},
|
||||
};
|
||||
|
||||
prompt.push_str(excerpts_preamble);
|
||||
prompt.push('\n');
|
||||
|
||||
let mut section_labels = Default::default();
|
||||
|
||||
if !request.referenced_declarations.is_empty() || !request.signatures.is_empty() {
|
||||
let syntax_based_prompt = SyntaxBasedPrompt::populate(request)?;
|
||||
section_labels = syntax_based_prompt.write(&mut insertions, &mut prompt)?;
|
||||
@@ -217,19 +277,38 @@ pub fn build_prompt(
|
||||
anyhow::bail!("PromptFormat::LabeledSections cannot be used with ContextMode::Llm");
|
||||
}
|
||||
|
||||
let include_line_numbers = matches!(
|
||||
request.prompt_format,
|
||||
PromptFormat::NumLinesUniDiff | PromptFormat::Minimal
|
||||
);
|
||||
for related_file in &request.included_files {
|
||||
write_codeblock(
|
||||
&related_file.path,
|
||||
&related_file.excerpts,
|
||||
if related_file.path == request.excerpt_path {
|
||||
&insertions
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
related_file.max_row,
|
||||
request.prompt_format == PromptFormat::NumLinesUniDiff,
|
||||
&mut prompt,
|
||||
);
|
||||
if request.prompt_format == PromptFormat::Minimal {
|
||||
write_codeblock_with_filename(
|
||||
&related_file.path,
|
||||
&related_file.excerpts,
|
||||
if related_file.path == request.excerpt_path {
|
||||
&insertions
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut prompt,
|
||||
);
|
||||
} else {
|
||||
write_codeblock(
|
||||
&related_file.path,
|
||||
&related_file.excerpts,
|
||||
if related_file.path == request.excerpt_path {
|
||||
&insertions
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut prompt,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +319,9 @@ pub fn build_prompt(
|
||||
PromptFormat::OldTextNewText => {
|
||||
prompt.push_str(OLD_TEXT_NEW_TEXT_REMINDER);
|
||||
}
|
||||
PromptFormat::Minimal => {
|
||||
prompt.push_str(MINIMAL_PROMPT_REMINDER);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -255,6 +337,27 @@ pub fn write_codeblock<'a>(
|
||||
output: &'a mut String,
|
||||
) {
|
||||
writeln!(output, "`````{}", DiffPathFmt(path)).unwrap();
|
||||
|
||||
write_excerpts(
|
||||
excerpts,
|
||||
sorted_insertions,
|
||||
file_line_count,
|
||||
include_line_numbers,
|
||||
output,
|
||||
);
|
||||
write!(output, "`````\n\n").unwrap();
|
||||
}
|
||||
|
||||
fn write_codeblock_with_filename<'a>(
|
||||
path: &Path,
|
||||
excerpts: impl IntoIterator<Item = &'a Excerpt>,
|
||||
sorted_insertions: &[(Point, &str)],
|
||||
file_line_count: Line,
|
||||
include_line_numbers: bool,
|
||||
output: &'a mut String,
|
||||
) {
|
||||
writeln!(output, "`````filename={}", DiffPathFmt(path)).unwrap();
|
||||
|
||||
write_excerpts(
|
||||
excerpts,
|
||||
sorted_insertions,
|
||||
@@ -666,6 +769,7 @@ impl<'a> SyntaxBasedPrompt<'a> {
|
||||
PromptFormat::MarkedExcerpt
|
||||
| PromptFormat::OnlySnippets
|
||||
| PromptFormat::OldTextNewText
|
||||
| PromptFormat::Minimal
|
||||
| PromptFormat::NumLinesUniDiff => {
|
||||
if range.start.0 > 0 && !skipped_last_snippet {
|
||||
output.push_str("…\n");
|
||||
@@ -681,6 +785,7 @@ impl<'a> SyntaxBasedPrompt<'a> {
|
||||
writeln!(output, "<|section_{}|>", section_index).ok();
|
||||
}
|
||||
}
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
}
|
||||
|
||||
let push_full_snippet = |output: &mut String| {
|
||||
@@ -790,3 +895,69 @@ fn declaration_size(declaration: &ReferencedDeclaration, style: DeclarationStyle
|
||||
DeclarationStyle::Declaration => declaration.text.len(),
|
||||
}
|
||||
}
|
||||
|
||||
struct MinimalQwenPrompt {
|
||||
events: Vec<Event>,
|
||||
cursor_point: Point,
|
||||
cursor_path: Arc<Path>, // TODO: make a common struct with cursor_point
|
||||
included_files: Vec<IncludedFile>,
|
||||
}
|
||||
|
||||
impl MinimalQwenPrompt {
|
||||
const INSTRUCTIONS: &str = "You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.\n";
|
||||
|
||||
fn render(&self) -> String {
|
||||
let edit_history = self.fmt_edit_history();
|
||||
let context = self.fmt_context();
|
||||
|
||||
format!(
|
||||
"{instructions}\n\n{edit_history}\n\n{context}",
|
||||
instructions = MinimalQwenPrompt::INSTRUCTIONS,
|
||||
edit_history = edit_history,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
|
||||
fn fmt_edit_history(&self) -> String {
|
||||
if self.events.is_empty() {
|
||||
"(No edit history)\n\n".to_string()
|
||||
} else {
|
||||
let mut events_str = String::new();
|
||||
push_events(&mut events_str, &self.events);
|
||||
format!(
|
||||
"The following are the latest edits made by the user, from earlier to later.\n\n{}",
|
||||
events_str
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_context(&self) -> String {
|
||||
let mut context = String::new();
|
||||
let include_line_numbers = true;
|
||||
|
||||
for related_file in &self.included_files {
|
||||
writeln!(context, "<|file_sep|>{}", DiffPathFmt(&related_file.path)).unwrap();
|
||||
|
||||
if related_file.path == self.cursor_path {
|
||||
write!(context, "<|fim_prefix|>").unwrap();
|
||||
write_excerpts(
|
||||
&related_file.excerpts,
|
||||
&[(self.cursor_point, "<|fim_suffix|>")],
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut context,
|
||||
);
|
||||
writeln!(context, "<|fim_middle|>").unwrap();
|
||||
} else {
|
||||
write_excerpts(
|
||||
&related_file.excerpts,
|
||||
&[],
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut context,
|
||||
);
|
||||
}
|
||||
}
|
||||
context
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ pub fn build_prompt(request: predict_edits_v3::PlanContextRetrievalRequest) -> R
|
||||
let mut prompt = SEARCH_INSTRUCTIONS.to_string();
|
||||
|
||||
if !request.events.is_empty() {
|
||||
writeln!(&mut prompt, "## User Edits\n")?;
|
||||
writeln!(&mut prompt, "\n## User Edits\n\n")?;
|
||||
push_events(&mut prompt, &request.events);
|
||||
}
|
||||
|
||||
writeln!(&mut prompt, "## Cursor context")?;
|
||||
writeln!(&mut prompt, "## Cursor context\n")?;
|
||||
write_codeblock(
|
||||
&request.excerpt_path,
|
||||
&[Excerpt {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
alter table billing_customers
|
||||
add column external_id text;
|
||||
|
||||
create unique index uix_billing_customers_on_external_id on billing_customers (external_id);
|
||||
@@ -1005,6 +1005,7 @@ impl Database {
|
||||
is_last_update: true,
|
||||
merge_message: db_repository_entry.merge_message,
|
||||
stash_entries: Vec::new(),
|
||||
renamed_paths: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,6 +796,7 @@ impl Database {
|
||||
is_last_update: true,
|
||||
merge_message: db_repository.merge_message,
|
||||
stash_entries: Vec::new(),
|
||||
renamed_paths: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
#[gpui::test]
|
||||
async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
@@ -307,17 +307,35 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
||||
let mut fake_language_servers = [
|
||||
client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
),
|
||||
client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "fake-analyzer",
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
),
|
||||
];
|
||||
client_b.language_registry().add(rust_lang());
|
||||
client_b.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
client_b.language_registry().add(rust_lang());
|
||||
client_b.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "fake-analyzer",
|
||||
capabilities,
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
@@ -352,7 +370,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)
|
||||
});
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
let fake_language_server = fake_language_servers[0].next().await.unwrap();
|
||||
let second_fake_language_server = fake_language_servers[1].next().await.unwrap();
|
||||
cx_a.background_executor.run_until_parked();
|
||||
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
@@ -414,6 +433,11 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
second_fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) })
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.executor().finish_waiting();
|
||||
|
||||
// Open the buffer on the host.
|
||||
@@ -522,6 +546,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
])))
|
||||
});
|
||||
|
||||
// Second language server also needs to handle the request (returns None)
|
||||
let mut second_completion_response = second_fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
|
||||
|
||||
// The completion now gets a new `text_edit.new_text` when resolving the completion item
|
||||
let mut resolve_completion_response = fake_language_server
|
||||
.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {
|
||||
@@ -545,6 +573,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
completion_response.next().await.unwrap();
|
||||
second_completion_response.next().await.unwrap();
|
||||
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
@@ -563,6 +592,77 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
|
||||
);
|
||||
});
|
||||
|
||||
// Ensure buffer is synced before proceeding with the next test
|
||||
cx_a.executor().run_until_parked();
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// Test completions from the second fake language server
|
||||
// Add another completion trigger to test the second language server
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([68..68])
|
||||
});
|
||||
editor.handle_input("; b", window, cx);
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b. }"
|
||||
);
|
||||
});
|
||||
|
||||
// Set up completion handlers for both language servers
|
||||
let mut first_lsp_completion = fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
|
||||
|
||||
let mut second_lsp_completion = second_fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
lsp::Position::new(1, 54),
|
||||
);
|
||||
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "analyzer_method(…)".into(),
|
||||
detail: Some("fn(&self) -> Result<T>".into()),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
new_text: "analyzer_method()".to_string(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(1, 54),
|
||||
lsp::Position::new(1, 54),
|
||||
),
|
||||
})),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// Await both language server responses
|
||||
first_lsp_completion.next().await.unwrap();
|
||||
second_lsp_completion.next().await.unwrap();
|
||||
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// Confirm the completion from the second language server works
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b.analyzer_method() }"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -2169,16 +2269,28 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
} else {
|
||||
"initial hint"
|
||||
};
|
||||
Ok(Some(vec![lsp::InlayHint {
|
||||
position: lsp::Position::new(0, character),
|
||||
label: lsp::InlayHintLabel::String(label.to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
}]))
|
||||
Ok(Some(vec![
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(0, character),
|
||||
label: lsp::InlayHintLabel::String(label.to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(1090, 1090),
|
||||
label: lsp::InlayHintLabel::String("out-of-bounds hint".to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
]))
|
||||
}
|
||||
})
|
||||
.next()
|
||||
|
||||
@@ -672,20 +672,25 @@ impl CollabPanel {
|
||||
{
|
||||
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
|
||||
}
|
||||
|
||||
let should_respect_collapse = query.is_empty();
|
||||
let mut collapse_depth = None;
|
||||
|
||||
for (idx, channel) in channels.into_iter().enumerate() {
|
||||
let depth = channel.parent_path.len();
|
||||
|
||||
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
|
||||
collapse_depth = Some(depth);
|
||||
} else if let Some(collapsed_depth) = collapse_depth {
|
||||
if depth > collapsed_depth {
|
||||
continue;
|
||||
}
|
||||
if self.is_channel_collapsed(channel.id) {
|
||||
if should_respect_collapse {
|
||||
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
|
||||
collapse_depth = Some(depth);
|
||||
} else {
|
||||
collapse_depth = None;
|
||||
} else if let Some(collapsed_depth) = collapse_depth {
|
||||
if depth > collapsed_depth {
|
||||
continue;
|
||||
}
|
||||
if self.is_channel_collapsed(channel.id) {
|
||||
collapse_depth = Some(depth);
|
||||
} else {
|
||||
collapse_depth = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ workspace = true
|
||||
path = "src/context_server.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
test-support = ["gpui/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -20,6 +20,7 @@ async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
log.workspace = true
|
||||
net.workspace = true
|
||||
parking_lot.workspace = true
|
||||
@@ -32,3 +33,6 @@ smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -6,6 +6,8 @@ pub mod test;
|
||||
pub mod transport;
|
||||
pub mod types;
|
||||
|
||||
use collections::HashMap;
|
||||
use http_client::HttpClient;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::{fmt::Display, path::PathBuf};
|
||||
@@ -15,6 +17,9 @@ use client::Client;
|
||||
use gpui::AsyncApp;
|
||||
use parking_lot::RwLock;
|
||||
pub use settings::ContextServerCommand;
|
||||
use url::Url;
|
||||
|
||||
use crate::transport::HttpTransport;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ContextServerId(pub Arc<str>);
|
||||
@@ -52,6 +57,25 @@ impl ContextServer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn http(
|
||||
id: ContextServerId,
|
||||
endpoint: &Url,
|
||||
headers: HashMap<String, String>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
executor: gpui::BackgroundExecutor,
|
||||
) -> Result<Self> {
|
||||
let transport = match endpoint.scheme() {
|
||||
"http" | "https" => {
|
||||
log::info!("Using HTTP transport for {}", endpoint);
|
||||
let transport =
|
||||
HttpTransport::new(http_client, endpoint.to_string(), headers, executor);
|
||||
Arc::new(transport) as _
|
||||
}
|
||||
_ => anyhow::bail!("unsupported MCP url scheme {}", endpoint.scheme()),
|
||||
};
|
||||
Ok(Self::new(id, transport))
|
||||
}
|
||||
|
||||
pub fn new(id: ContextServerId, transport: Arc<dyn crate::transport::Transport>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
pub mod http;
|
||||
mod stdio_transport;
|
||||
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use futures::Stream;
|
||||
use std::pin::Pin;
|
||||
|
||||
pub use http::*;
|
||||
pub use stdio_transport::*;
|
||||
|
||||
#[async_trait]
|
||||
|
||||
259
crates/context_server/src/transport/http.rs
Normal file
259
crates/context_server/src/transport/http.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use futures::{Stream, StreamExt};
|
||||
use gpui::BackgroundExecutor;
|
||||
use http_client::{AsyncBody, HttpClient, Request, Response, http::Method};
|
||||
use parking_lot::Mutex as SyncMutex;
|
||||
use smol::channel;
|
||||
use std::{pin::Pin, sync::Arc};
|
||||
|
||||
use crate::transport::Transport;
|
||||
|
||||
// Constants from MCP spec
|
||||
const HEADER_SESSION_ID: &str = "Mcp-Session-Id";
|
||||
const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream";
|
||||
const JSON_MIME_TYPE: &str = "application/json";
|
||||
|
||||
/// HTTP Transport with session management and SSE support
|
||||
pub struct HttpTransport {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
endpoint: String,
|
||||
session_id: Arc<SyncMutex<Option<String>>>,
|
||||
executor: BackgroundExecutor,
|
||||
response_tx: channel::Sender<String>,
|
||||
response_rx: channel::Receiver<String>,
|
||||
error_tx: channel::Sender<String>,
|
||||
error_rx: channel::Receiver<String>,
|
||||
// Authentication headers to include in requests
|
||||
headers: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl HttpTransport {
|
||||
pub fn new(
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
endpoint: String,
|
||||
headers: HashMap<String, String>,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Self {
|
||||
let (response_tx, response_rx) = channel::unbounded();
|
||||
let (error_tx, error_rx) = channel::unbounded();
|
||||
|
||||
Self {
|
||||
http_client,
|
||||
executor,
|
||||
endpoint,
|
||||
session_id: Arc::new(SyncMutex::new(None)),
|
||||
response_tx,
|
||||
response_rx,
|
||||
error_tx,
|
||||
error_rx,
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a message and handle the response based on content type
|
||||
async fn send_message(&self, message: String) -> Result<()> {
|
||||
let is_notification =
|
||||
!message.contains("\"id\":") || message.contains("notifications/initialized");
|
||||
|
||||
let mut request_builder = Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(&self.endpoint)
|
||||
.header("Content-Type", JSON_MIME_TYPE)
|
||||
.header(
|
||||
"Accept",
|
||||
format!("{}, {}", JSON_MIME_TYPE, EVENT_STREAM_MIME_TYPE),
|
||||
);
|
||||
|
||||
for (key, value) in &self.headers {
|
||||
request_builder = request_builder.header(key.as_str(), value.as_str());
|
||||
}
|
||||
|
||||
// Add session ID if we have one (except for initialize)
|
||||
if let Some(ref session_id) = *self.session_id.lock() {
|
||||
request_builder = request_builder.header(HEADER_SESSION_ID, session_id.as_str());
|
||||
}
|
||||
|
||||
let request = request_builder.body(AsyncBody::from(message.into_bytes()))?;
|
||||
let mut response = self.http_client.send(request).await?;
|
||||
|
||||
// Handle different response types based on status and content-type
|
||||
match response.status() {
|
||||
status if status.is_success() => {
|
||||
// Check content type
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
// Extract session ID from response headers if present
|
||||
if let Some(session_id) = response
|
||||
.headers()
|
||||
.get(HEADER_SESSION_ID)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
{
|
||||
*self.session_id.lock() = Some(session_id.to_string());
|
||||
log::debug!("Session ID set: {}", session_id);
|
||||
}
|
||||
|
||||
match content_type {
|
||||
Some(ct) if ct.starts_with(JSON_MIME_TYPE) => {
|
||||
// JSON response - read and forward immediately
|
||||
let mut body = String::new();
|
||||
futures::AsyncReadExt::read_to_string(response.body_mut(), &mut body)
|
||||
.await?;
|
||||
|
||||
// Only send non-empty responses
|
||||
if !body.is_empty() {
|
||||
self.response_tx
|
||||
.send(body)
|
||||
.await
|
||||
.map_err(|_| anyhow!("Failed to send JSON response"))?;
|
||||
}
|
||||
}
|
||||
Some(ct) if ct.starts_with(EVENT_STREAM_MIME_TYPE) => {
|
||||
// SSE stream - set up streaming
|
||||
self.setup_sse_stream(response).await?;
|
||||
}
|
||||
_ => {
|
||||
// For notifications, 202 Accepted with no content type is ok
|
||||
if is_notification && status.as_u16() == 202 {
|
||||
log::debug!("Notification accepted");
|
||||
} else {
|
||||
return Err(anyhow!("Unexpected content type: {:?}", content_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
status if status.as_u16() == 202 => {
|
||||
// Accepted - notification acknowledged, no response needed
|
||||
log::debug!("Notification accepted");
|
||||
}
|
||||
_ => {
|
||||
let mut error_body = String::new();
|
||||
futures::AsyncReadExt::read_to_string(response.body_mut(), &mut error_body).await?;
|
||||
|
||||
self.error_tx
|
||||
.send(format!("HTTP {}: {}", response.status(), error_body))
|
||||
.await
|
||||
.map_err(|_| anyhow!("Failed to send error"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set up SSE streaming from the response
|
||||
async fn setup_sse_stream(&self, mut response: Response<AsyncBody>) -> Result<()> {
|
||||
let response_tx = self.response_tx.clone();
|
||||
let error_tx = self.error_tx.clone();
|
||||
|
||||
// Spawn a task to handle the SSE stream
|
||||
smol::spawn(async move {
|
||||
let reader = futures::io::BufReader::new(response.body_mut());
|
||||
let mut lines = futures::AsyncBufReadExt::lines(reader);
|
||||
|
||||
let mut data_buffer = Vec::new();
|
||||
let mut in_message = false;
|
||||
|
||||
while let Some(line_result) = lines.next().await {
|
||||
match line_result {
|
||||
Ok(line) => {
|
||||
if line.is_empty() {
|
||||
// Empty line signals end of event
|
||||
if !data_buffer.is_empty() {
|
||||
let message = data_buffer.join("\n");
|
||||
|
||||
// Filter out ping messages and empty data
|
||||
if !message.trim().is_empty() && message != "ping" {
|
||||
if let Err(e) = response_tx.send(message).await {
|
||||
log::error!("Failed to send SSE message: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
data_buffer.clear();
|
||||
}
|
||||
in_message = false;
|
||||
} else if let Some(data) = line.strip_prefix("data: ") {
|
||||
// Handle data lines
|
||||
let data = data.trim();
|
||||
if !data.is_empty() {
|
||||
// Check if this is a ping message
|
||||
if data == "ping" {
|
||||
log::trace!("Received SSE ping");
|
||||
continue;
|
||||
}
|
||||
data_buffer.push(data.to_string());
|
||||
in_message = true;
|
||||
}
|
||||
} else if line.starts_with("event:")
|
||||
|| line.starts_with("id:")
|
||||
|| line.starts_with("retry:")
|
||||
{
|
||||
// Ignore other SSE fields
|
||||
continue;
|
||||
} else if in_message {
|
||||
// Continuation of data
|
||||
data_buffer.push(line);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = error_tx.send(format!("SSE stream error: {}", e)).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Transport for HttpTransport {
|
||||
async fn send(&self, message: String) -> Result<()> {
|
||||
self.send_message(message).await
|
||||
}
|
||||
|
||||
fn receive(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
|
||||
Box::pin(self.response_rx.clone())
|
||||
}
|
||||
|
||||
fn receive_err(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
|
||||
Box::pin(self.error_rx.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HttpTransport {
|
||||
fn drop(&mut self) {
|
||||
// Try to cleanup session on drop
|
||||
let http_client = self.http_client.clone();
|
||||
let endpoint = self.endpoint.clone();
|
||||
let session_id = self.session_id.lock().clone();
|
||||
let headers = self.headers.clone();
|
||||
|
||||
if let Some(session_id) = session_id {
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let mut request_builder = Request::builder()
|
||||
.method(Method::DELETE)
|
||||
.uri(&endpoint)
|
||||
.header(HEADER_SESSION_ID, &session_id);
|
||||
|
||||
// Add authentication headers if present
|
||||
for (key, value) in headers {
|
||||
request_builder = request_builder.header(key.as_str(), value.as_str());
|
||||
}
|
||||
|
||||
let request = request_builder.body(AsyncBody::empty());
|
||||
|
||||
if let Ok(request) = request {
|
||||
let _ = http_client.send(request).await;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1029,13 +1029,11 @@ impl SearchableItem for DapLogView {
|
||||
&mut self,
|
||||
index: usize,
|
||||
matches: &[Self::Match],
|
||||
collapse: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |e, cx| {
|
||||
e.activate_match(index, matches, collapse, window, cx)
|
||||
})
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.activate_match(index, matches, window, cx))
|
||||
}
|
||||
|
||||
fn select_matches(
|
||||
|
||||
@@ -491,7 +491,7 @@ impl ProjectDiagnosticsEditor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let was_empty = self.multibuffer.read(cx).is_empty();
|
||||
let mut buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
|
||||
let max_severity = if self.include_warnings {
|
||||
@@ -602,7 +602,6 @@ impl ProjectDiagnosticsEditor {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
buffer_snapshot = cx.update(|_, cx| buffer.read(cx).snapshot())?;
|
||||
let initial_range = buffer_snapshot.anchor_after(b.initial_range.start)
|
||||
..buffer_snapshot.anchor_before(b.initial_range.end);
|
||||
let excerpt_range = ExcerptRange {
|
||||
@@ -1010,11 +1009,14 @@ async fn heuristic_syntactic_expand(
|
||||
snapshot: BufferSnapshot,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<RangeInclusive<BufferRow>> {
|
||||
let start = snapshot.clip_point(input_range.start, Bias::Right);
|
||||
let end = snapshot.clip_point(input_range.end, Bias::Left);
|
||||
let input_row_count = input_range.end.row - input_range.start.row;
|
||||
if input_row_count > max_row_count {
|
||||
return None;
|
||||
}
|
||||
|
||||
let input_range = start..end;
|
||||
// If the outline node contains the diagnostic and is small enough, just use that.
|
||||
let outline_range = snapshot.outline_range_containing(input_range.clone());
|
||||
if let Some(outline_range) = outline_range.clone() {
|
||||
|
||||
@@ -18,18 +18,19 @@ client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
codestral.workspace = true
|
||||
copilot.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
language.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
settings.workspace = true
|
||||
supermaven.workspace = true
|
||||
sweep_ai.workspace = true
|
||||
telemetry.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -18,12 +18,15 @@ use language::{
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use regex::Regex;
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use settings::{
|
||||
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME, Settings, SettingsStore, update_settings_file,
|
||||
};
|
||||
use std::{
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
use supermaven::{AccountStatus, Supermaven};
|
||||
use sweep_ai::SweepFeatureFlag;
|
||||
use ui::{
|
||||
Clickable, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton,
|
||||
IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
|
||||
@@ -78,7 +81,7 @@ impl Render for EditPredictionButton {
|
||||
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
|
||||
match all_language_settings.edit_predictions.provider {
|
||||
match &all_language_settings.edit_predictions.provider {
|
||||
EditPredictionProvider::None => div().hidden(),
|
||||
|
||||
EditPredictionProvider::Copilot => {
|
||||
@@ -297,6 +300,15 @@ impl Render for EditPredictionButton {
|
||||
.with_handle(self.popover_menu_handle.clone()),
|
||||
)
|
||||
}
|
||||
EditPredictionProvider::Experimental(provider_name) => {
|
||||
if *provider_name == EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME
|
||||
&& cx.has_flag::<SweepFeatureFlag>()
|
||||
{
|
||||
div().child(Icon::new(IconName::SweepAi))
|
||||
} else {
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
||||
EditPredictionProvider::Zed => {
|
||||
let enabled = self.editor_enabled.unwrap_or(true);
|
||||
@@ -525,7 +537,7 @@ impl EditPredictionButton {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
})
|
||||
}
|
||||
EditPredictionProvider::None => continue,
|
||||
EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => continue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1099,6 +1099,7 @@ pub struct Editor {
|
||||
searchable: bool,
|
||||
cursor_shape: CursorShape,
|
||||
current_line_highlight: Option<CurrentLineHighlight>,
|
||||
collapse_matches: bool,
|
||||
autoindent_mode: Option<AutoindentMode>,
|
||||
workspace: Option<(WeakEntity<Workspace>, Option<WorkspaceId>)>,
|
||||
input_enabled: bool,
|
||||
@@ -2211,7 +2212,7 @@ impl Editor {
|
||||
.unwrap_or_default(),
|
||||
current_line_highlight: None,
|
||||
autoindent_mode: Some(AutoindentMode::EachLine),
|
||||
|
||||
collapse_matches: false,
|
||||
workspace: None,
|
||||
input_enabled: !is_minimap,
|
||||
use_modal_editing: full_mode,
|
||||
@@ -2384,7 +2385,10 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
EditorEvent::Edited { .. } => {
|
||||
if vim_flavor(cx).is_none() {
|
||||
let vim_mode = vim_mode_setting::VimModeSetting::try_get(cx)
|
||||
.map(|vim_mode| vim_mode.0)
|
||||
.unwrap_or(false);
|
||||
if !vim_mode {
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let selections = editor.selections.all_adjusted_display(&display_map);
|
||||
let pop_state = editor
|
||||
@@ -2631,6 +2635,10 @@ impl Editor {
|
||||
key_context.add("end_of_input");
|
||||
}
|
||||
|
||||
if self.has_any_expanded_diff_hunks(cx) {
|
||||
key_context.add("diffs_expanded");
|
||||
}
|
||||
|
||||
key_context
|
||||
}
|
||||
|
||||
@@ -3009,17 +3017,21 @@ impl Editor {
|
||||
self.current_line_highlight = current_line_highlight;
|
||||
}
|
||||
|
||||
pub fn range_for_match<T: std::marker::Copy>(
|
||||
&self,
|
||||
range: &Range<T>,
|
||||
collapse: bool,
|
||||
) -> Range<T> {
|
||||
if collapse {
|
||||
pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
|
||||
self.collapse_matches = collapse_matches;
|
||||
}
|
||||
|
||||
pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
|
||||
if self.collapse_matches {
|
||||
return range.start..range.start;
|
||||
}
|
||||
range.clone()
|
||||
}
|
||||
|
||||
pub fn clip_at_line_ends(&mut self, cx: &mut Context<Self>) -> bool {
|
||||
self.display_map.read(cx).clip_at_line_ends
|
||||
}
|
||||
|
||||
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut Context<Self>) {
|
||||
if self.display_map.read(cx).clip_at_line_ends != clip {
|
||||
self.display_map
|
||||
@@ -16917,7 +16929,7 @@ impl Editor {
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
let range = target_range.to_point(target_buffer.read(cx));
|
||||
let range = editor.range_for_match(&range, false);
|
||||
let range = editor.range_for_match(&range);
|
||||
let range = collapse_multiline_range(range);
|
||||
|
||||
if !split
|
||||
@@ -19336,6 +19348,16 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn has_any_expanded_diff_hunks(&self, cx: &App) -> bool {
|
||||
if self.buffer.read(cx).all_diff_hunks_expanded() {
|
||||
return true;
|
||||
}
|
||||
let ranges = vec![Anchor::min()..Anchor::max()];
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.has_expanded_diff_hunks_in_ranges(&ranges, cx)
|
||||
}
|
||||
|
||||
fn toggle_diff_hunks_in_ranges(
|
||||
&mut self,
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
@@ -21747,7 +21769,9 @@ impl Editor {
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|a| a.to_string()));
|
||||
|
||||
let vim_mode = vim_flavor(cx).is_some();
|
||||
let vim_mode = vim_mode_setting::VimModeSetting::try_get(cx)
|
||||
.map(|vim_mode| vim_mode.0)
|
||||
.unwrap_or(false);
|
||||
|
||||
let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider;
|
||||
let copilot_enabled = edit_predictions_provider
|
||||
@@ -22382,28 +22406,6 @@ fn edit_for_markdown_paste<'a>(
|
||||
(range, new_text)
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum VimFlavor {
|
||||
Vim,
|
||||
Helix,
|
||||
}
|
||||
|
||||
pub fn vim_flavor(cx: &App) -> Option<VimFlavor> {
|
||||
if vim_mode_setting::HelixModeSetting::try_get(cx)
|
||||
.map(|helix_mode| helix_mode.0)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(VimFlavor::Helix)
|
||||
} else if vim_mode_setting::VimModeSetting::try_get(cx)
|
||||
.map(|vim_mode| vim_mode.0)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(VimFlavor::Vim)
|
||||
} else {
|
||||
None // neither vim nor helix mode
|
||||
}
|
||||
}
|
||||
|
||||
fn process_completion_for_edit(
|
||||
completion: &Completion,
|
||||
intent: CompletionIntent,
|
||||
|
||||
@@ -32,6 +32,7 @@ use language::{
|
||||
tree_sitter_python,
|
||||
};
|
||||
use language_settings::Formatter;
|
||||
use languages::markdown_lang;
|
||||
use languages::rust_lang;
|
||||
use lsp::CompletionParams;
|
||||
use multi_buffer::{IndentGuide, PathKey};
|
||||
@@ -17503,6 +17504,89 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
|
||||
language_registry.add(markdown_lang());
|
||||
language_registry.add(rust_lang());
|
||||
let buffer = cx.new(|cx| {
|
||||
let mut buffer = language::Buffer::local(
|
||||
indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}
|
||||
}
|
||||
```
|
||||
"},
|
||||
cx,
|
||||
);
|
||||
buffer.set_language_registry(language_registry.clone());
|
||||
buffer.set_language(Some(markdown_lang()), cx);
|
||||
buffer
|
||||
});
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Case 1: Test outer enclosing brackets
|
||||
select_ranges(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}
|
||||
}ˇ
|
||||
```
|
||||
"},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree ˇ{
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}
|
||||
}
|
||||
```
|
||||
"},
|
||||
cx,
|
||||
);
|
||||
// Case 2: Test inner enclosing brackets
|
||||
select_ranges(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}ˇ
|
||||
}
|
||||
```
|
||||
"},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
|
||||
}
|
||||
}
|
||||
```
|
||||
"},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
collections::hash_map,
|
||||
ops::{ControlFlow, Range},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -778,6 +779,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
let excerpts = self.buffer.read(cx).excerpt_ids();
|
||||
let mut inserted_hint_text = HashMap::default();
|
||||
let hints_to_insert = new_hints
|
||||
.into_iter()
|
||||
.filter_map(|(chunk_range, hints_result)| {
|
||||
@@ -804,8 +806,35 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
})
|
||||
.flat_map(|hints| hints.into_values())
|
||||
.flatten()
|
||||
.flat_map(|new_hints| {
|
||||
let mut hints_deduplicated = Vec::new();
|
||||
|
||||
if new_hints.len() > 1 {
|
||||
for (server_id, new_hints) in new_hints {
|
||||
for (new_id, new_hint) in new_hints {
|
||||
let hints_text_for_position = inserted_hint_text
|
||||
.entry(new_hint.position)
|
||||
.or_insert_with(HashMap::default);
|
||||
let insert =
|
||||
match hints_text_for_position.entry(new_hint.text().to_string()) {
|
||||
hash_map::Entry::Occupied(o) => o.get() == &server_id,
|
||||
hash_map::Entry::Vacant(v) => {
|
||||
v.insert(server_id);
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if insert {
|
||||
hints_deduplicated.push((new_id, new_hint));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hints_deduplicated.extend(new_hints.into_values().flatten());
|
||||
}
|
||||
|
||||
hints_deduplicated
|
||||
})
|
||||
.filter_map(|(hint_id, lsp_hint)| {
|
||||
if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
|
||||
&& inlay_hints
|
||||
@@ -3732,6 +3761,7 @@ let c = 3;"#
|
||||
let mut fake_servers = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "rust-analyzer",
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
@@ -3804,6 +3834,78 @@ let c = 3;"#
|
||||
},
|
||||
);
|
||||
|
||||
// Add another server that does send the same, duplicate hints back
|
||||
let mut fake_servers_2 = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "CrabLang-ls",
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
initializer: Some(Box::new(move |fake_server| {
|
||||
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
|
||||
move |params, _| async move {
|
||||
if params.text_document.uri
|
||||
== lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
|
||||
{
|
||||
Ok(Some(vec![
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(1, 9),
|
||||
label: lsp::InlayHintLabel::String(": i32".to_owned()),
|
||||
kind: Some(lsp::InlayHintKind::TYPE),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(19, 9),
|
||||
label: lsp::InlayHintLabel::String(": i33".to_owned()),
|
||||
kind: Some(lsp::InlayHintKind::TYPE),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
]))
|
||||
} else if params.text_document.uri
|
||||
== lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
|
||||
{
|
||||
Ok(Some(vec![
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(1, 10),
|
||||
label: lsp::InlayHintLabel::String(": i34".to_owned()),
|
||||
kind: Some(lsp::InlayHintKind::TYPE),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(29, 10),
|
||||
label: lsp::InlayHintLabel::String(": i35".to_owned()),
|
||||
kind: Some(lsp::InlayHintKind::TYPE),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
]))
|
||||
} else {
|
||||
panic!("Unexpected file path {:?}", params.text_document.uri);
|
||||
}
|
||||
},
|
||||
);
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
let (buffer_1, _handle_1) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
|
||||
@@ -3847,6 +3949,7 @@ let c = 3;"#
|
||||
});
|
||||
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
let _fake_server_2 = fake_servers_2.next().await.unwrap();
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
@@ -3855,11 +3958,16 @@ let c = 3;"#
|
||||
assert_eq!(
|
||||
vec![
|
||||
": i32".to_string(),
|
||||
": i32".to_string(),
|
||||
": i33".to_string(),
|
||||
": i33".to_string(),
|
||||
": i34".to_string(),
|
||||
": i34".to_string(),
|
||||
": i35".to_string(),
|
||||
": i35".to_string(),
|
||||
],
|
||||
sorted_cached_hint_labels(editor, cx),
|
||||
"We receive duplicate hints from 2 servers and cache them all"
|
||||
);
|
||||
assert_eq!(
|
||||
vec![
|
||||
@@ -3869,7 +3977,7 @@ let c = 3;"#
|
||||
": i33".to_string(),
|
||||
],
|
||||
visible_hint_labels(editor, cx),
|
||||
"lib.rs is added before main.rs , so its excerpts should be visible first"
|
||||
"lib.rs is added before main.rs , so its excerpts should be visible first; hints should be deduplicated per label"
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -3919,8 +4027,12 @@ let c = 3;"#
|
||||
assert_eq!(
|
||||
vec![
|
||||
": i32".to_string(),
|
||||
": i32".to_string(),
|
||||
": i33".to_string(),
|
||||
": i33".to_string(),
|
||||
": i34".to_string(),
|
||||
": i34".to_string(),
|
||||
": i35".to_string(),
|
||||
": i35".to_string(),
|
||||
],
|
||||
sorted_cached_hint_labels(editor, cx),
|
||||
|
||||
@@ -1586,12 +1586,11 @@ impl SearchableItem for Editor {
|
||||
&mut self,
|
||||
index: usize,
|
||||
matches: &[Range<Anchor>],
|
||||
collapse: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.unfold_ranges(&[matches[index].clone()], false, true, cx);
|
||||
let range = self.range_for_match(&matches[index], collapse);
|
||||
let range = self.range_for_match(&matches[index]);
|
||||
let autoscroll = if EditorSettings::get_global(cx).search.center_on_match {
|
||||
Autoscroll::center()
|
||||
} else {
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use language::rust_lang;
|
||||
use language::{markdown_lang, rust_lang};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Editor, ToPoint};
|
||||
@@ -313,6 +313,22 @@ impl EditorLspTestContext {
|
||||
Self::new(language, Default::default(), cx).await
|
||||
}
|
||||
|
||||
pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> Self {
|
||||
let context = Self::new(
|
||||
Arc::into_inner(markdown_lang()).unwrap(),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let language_registry = context.workspace.read_with(cx, |workspace, cx| {
|
||||
workspace.project().read(cx).languages().clone()
|
||||
});
|
||||
language_registry.add(rust_lang());
|
||||
|
||||
context
|
||||
}
|
||||
|
||||
/// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||
#[track_caller]
|
||||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||
|
||||
@@ -25,6 +25,7 @@ language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
parking_lot.workspace = true
|
||||
proto.workspace = true
|
||||
semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -193,6 +193,36 @@ pub struct TargetConfig {
|
||||
/// If not provided and the URL is a GitHub release, we'll attempt to fetch it from GitHub.
|
||||
#[serde(default)]
|
||||
pub sha256: Option<String>,
|
||||
/// Environment variables to set when launching the agent server.
|
||||
/// These target-specific env vars will override any env vars set at the agent level.
|
||||
#[serde(default)]
|
||||
pub env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl TargetConfig {
|
||||
pub fn from_proto(proto: proto::ExternalExtensionAgentTarget) -> Self {
|
||||
Self {
|
||||
archive: proto.archive,
|
||||
cmd: proto.cmd,
|
||||
args: proto.args,
|
||||
sha256: proto.sha256,
|
||||
env: proto.env.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::ExternalExtensionAgentTarget {
|
||||
proto::ExternalExtensionAgentTarget {
|
||||
archive: self.archive.clone(),
|
||||
cmd: self.cmd.clone(),
|
||||
args: self.args.clone(),
|
||||
sha256: self.sha256.clone(),
|
||||
env: self
|
||||
.env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
@@ -265,25 +295,26 @@ impl ExtensionManifest {
|
||||
.and_then(OsStr::to_str)
|
||||
.context("invalid extension name")?;
|
||||
|
||||
let mut extension_manifest_path = extension_dir.join("extension.json");
|
||||
let extension_manifest_path = extension_dir.join("extension.toml");
|
||||
if fs.is_file(&extension_manifest_path).await {
|
||||
let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| {
|
||||
format!("loading {extension_name} extension.json, {extension_manifest_path:?}")
|
||||
})?;
|
||||
let manifest_json = serde_json::from_str::<OldExtensionManifest>(&manifest_content)
|
||||
.with_context(|| {
|
||||
format!("invalid extension.json for extension {extension_name}")
|
||||
})?;
|
||||
|
||||
Ok(manifest_from_old_manifest(manifest_json, extension_name))
|
||||
} else {
|
||||
extension_manifest_path.set_extension("toml");
|
||||
let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| {
|
||||
format!("loading {extension_name} extension.toml, {extension_manifest_path:?}")
|
||||
})?;
|
||||
toml::from_str(&manifest_content).map_err(|err| {
|
||||
anyhow!("Invalid extension.toml for extension {extension_name}:\n{err}")
|
||||
})
|
||||
} else if let extension_manifest_path = extension_manifest_path.with_extension("json")
|
||||
&& fs.is_file(&extension_manifest_path).await
|
||||
{
|
||||
let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| {
|
||||
format!("loading {extension_name} extension.json, {extension_manifest_path:?}")
|
||||
})?;
|
||||
|
||||
serde_json::from_str::<OldExtensionManifest>(&manifest_content)
|
||||
.with_context(|| format!("invalid extension.json for extension {extension_name}"))
|
||||
.map(|manifest_json| manifest_from_old_manifest(manifest_json, extension_name))
|
||||
} else {
|
||||
anyhow::bail!("No extension manifest found for extension {extension_name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use client::ExtensionProvides;
|
||||
use client::{Client, ExtensionMetadata, GetExtensionsResponse, proto, telemetry::Telemetry};
|
||||
use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map};
|
||||
use collections::{BTreeMap, BTreeSet, HashSet, btree_map};
|
||||
pub use extension::ExtensionManifest;
|
||||
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||
use extension::{
|
||||
@@ -43,7 +43,7 @@ use language::{
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::{RemoteClient, RemoteConnectionOptions};
|
||||
use remote::RemoteClient;
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -123,7 +123,7 @@ pub struct ExtensionStore {
|
||||
pub wasm_host: Arc<WasmHost>,
|
||||
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
|
||||
pub tasks: Vec<Task<()>>,
|
||||
pub remote_clients: HashMap<RemoteConnectionOptions, WeakEntity<RemoteClient>>,
|
||||
pub remote_clients: Vec<WeakEntity<RemoteClient>>,
|
||||
pub ssh_registered_tx: UnboundedSender<()>,
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ impl ExtensionStore {
|
||||
reload_tx,
|
||||
tasks: Vec::new(),
|
||||
|
||||
remote_clients: HashMap::default(),
|
||||
remote_clients: Default::default(),
|
||||
ssh_registered_tx: connection_registered_tx,
|
||||
};
|
||||
|
||||
@@ -343,12 +343,12 @@ impl ExtensionStore {
|
||||
let index = this
|
||||
.update(cx, |this, cx| this.rebuild_extension_index(cx))?
|
||||
.await;
|
||||
this.update( cx, |this, cx| this.extensions_updated(index, cx))?
|
||||
this.update(cx, |this, cx| this.extensions_updated(index, cx))?
|
||||
.await;
|
||||
index_changed = false;
|
||||
}
|
||||
|
||||
Self::update_ssh_clients(&this, cx).await?;
|
||||
Self::update_remote_clients(&this, cx).await?;
|
||||
}
|
||||
_ = connection_registered_rx.next() => {
|
||||
debounce_timer = cx
|
||||
@@ -758,29 +758,28 @@ impl ExtensionStore {
|
||||
if let Some(content_length) = content_length {
|
||||
let actual_len = tar_gz_bytes.len();
|
||||
if content_length != actual_len {
|
||||
bail!("downloaded extension size {actual_len} does not match content length {content_length}");
|
||||
bail!(concat!(
|
||||
"downloaded extension size {actual_len} ",
|
||||
"does not match content length {content_length}"
|
||||
));
|
||||
}
|
||||
}
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(tar_gz_bytes.as_slice()));
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(extension_dir).await?;
|
||||
this.update( cx, |this, cx| {
|
||||
this.reload(Some(extension_id.clone()), cx)
|
||||
})?
|
||||
.await;
|
||||
this.update(cx, |this, cx| this.reload(Some(extension_id.clone()), cx))?
|
||||
.await;
|
||||
|
||||
if let ExtensionOperation::Install = operation {
|
||||
this.update( cx, |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
cx.emit(Event::ExtensionInstalled(extension_id.clone()));
|
||||
if let Some(events) = ExtensionEvents::try_global(cx)
|
||||
&& let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
|
||||
events.update(cx, |this, cx| {
|
||||
this.emit(
|
||||
extension::Event::ExtensionInstalled(manifest.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
&& let Some(manifest) = this.extension_manifest_for_id(&extension_id)
|
||||
{
|
||||
events.update(cx, |this, cx| {
|
||||
this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -1726,7 +1725,7 @@ impl ExtensionStore {
|
||||
})
|
||||
}
|
||||
|
||||
async fn sync_extensions_over_ssh(
|
||||
async fn sync_extensions_to_remotes(
|
||||
this: &WeakEntity<Self>,
|
||||
client: WeakEntity<RemoteClient>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -1779,7 +1778,11 @@ impl ExtensionStore {
|
||||
})?,
|
||||
path_style,
|
||||
);
|
||||
log::info!("Uploading extension {}", missing_extension.clone().id);
|
||||
log::info!(
|
||||
"Uploading extension {} to {:?}",
|
||||
missing_extension.clone().id,
|
||||
dest_dir
|
||||
);
|
||||
|
||||
client
|
||||
.update(cx, |client, cx| {
|
||||
@@ -1792,27 +1795,35 @@ impl ExtensionStore {
|
||||
missing_extension.clone().id
|
||||
);
|
||||
|
||||
client
|
||||
let result = client
|
||||
.update(cx, |client, _cx| {
|
||||
client.proto_client().request(proto::InstallExtension {
|
||||
tmp_dir: dest_dir.to_proto(),
|
||||
extension: Some(missing_extension),
|
||||
extension: Some(missing_extension.clone()),
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
log::error!(
|
||||
"Failed to install extension {}: {}",
|
||||
missing_extension.id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_ssh_clients(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
|
||||
pub async fn update_remote_clients(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
|
||||
let clients = this.update(cx, |this, _cx| {
|
||||
this.remote_clients.retain(|_k, v| v.upgrade().is_some());
|
||||
this.remote_clients.values().cloned().collect::<Vec<_>>()
|
||||
this.remote_clients.retain(|v| v.upgrade().is_some());
|
||||
this.remote_clients.clone()
|
||||
})?;
|
||||
|
||||
for client in clients {
|
||||
Self::sync_extensions_over_ssh(this, client, cx)
|
||||
Self::sync_extensions_to_remotes(this, client, cx)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
@@ -1820,16 +1831,12 @@ impl ExtensionStore {
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub fn register_remote_client(&mut self, client: Entity<RemoteClient>, cx: &mut Context<Self>) {
|
||||
let options = client.read(cx).connection_options();
|
||||
|
||||
if let Some(existing_client) = self.remote_clients.get(&options)
|
||||
&& existing_client.upgrade().is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.remote_clients.insert(options, client.downgrade());
|
||||
pub fn register_remote_client(
|
||||
&mut self,
|
||||
client: Entity<RemoteClient>,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
self.remote_clients.push(client.downgrade());
|
||||
self.ssh_registered_tx.unbounded_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +279,8 @@ impl HeadlessExtensionStore {
|
||||
}
|
||||
|
||||
fs.rename(&tmp_path, &path, RenameOptions::default())
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to rename {tmp_path:?} to {path:?}")?;
|
||||
|
||||
Self::load_extension(this, extension, cx).await
|
||||
})
|
||||
|
||||
@@ -990,6 +990,9 @@ impl ExtensionImports for WasmState {
|
||||
command: None,
|
||||
settings: Some(settings),
|
||||
})?),
|
||||
project::project_settings::ContextServerSettings::Http { .. } => {
|
||||
bail!("remote context server settings not supported in 0.6.0")
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -359,6 +359,7 @@ impl GitRepository for FakeGitRepository {
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
anyhow::Ok(GitStatus {
|
||||
entries: entries.into(),
|
||||
renamed_paths: HashMap::default(),
|
||||
})
|
||||
});
|
||||
Task::ready(match result {
|
||||
|
||||
@@ -395,19 +395,19 @@ mod tests {
|
||||
thread::spawn(move || stream.run(move |events| tx.send(events.to_vec()).is_ok()));
|
||||
|
||||
fs::write(path.join("new-file"), "").unwrap();
|
||||
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
let events = rx.recv_timeout(timeout()).unwrap();
|
||||
let event = events.last().unwrap();
|
||||
assert_eq!(event.path, path.join("new-file"));
|
||||
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
|
||||
|
||||
fs::remove_file(path.join("existing-file-5")).unwrap();
|
||||
let mut events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
let mut events = rx.recv_timeout(timeout()).unwrap();
|
||||
let mut event = events.last().unwrap();
|
||||
// we see this duplicate about 1/100 test runs.
|
||||
if event.path == path.join("new-file")
|
||||
&& event.flags.contains(StreamFlags::ITEM_CREATED)
|
||||
{
|
||||
events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
events = rx.recv_timeout(timeout()).unwrap();
|
||||
event = events.last().unwrap();
|
||||
}
|
||||
assert_eq!(event.path, path.join("existing-file-5"));
|
||||
@@ -440,13 +440,13 @@ mod tests {
|
||||
});
|
||||
|
||||
fs::write(path.join("new-file"), "").unwrap();
|
||||
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
let events = rx.recv_timeout(timeout()).unwrap();
|
||||
let event = events.last().unwrap();
|
||||
assert_eq!(event.path, path.join("new-file"));
|
||||
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
|
||||
|
||||
fs::remove_file(path.join("existing-file-5")).unwrap();
|
||||
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
let events = rx.recv_timeout(timeout()).unwrap();
|
||||
let event = events.last().unwrap();
|
||||
assert_eq!(event.path, path.join("existing-file-5"));
|
||||
assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
|
||||
@@ -477,11 +477,11 @@ mod tests {
|
||||
});
|
||||
|
||||
fs::write(path.join("new-file"), "").unwrap();
|
||||
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "running");
|
||||
assert_eq!(rx.recv_timeout(timeout()).unwrap(), "running");
|
||||
|
||||
// Dropping the handle causes `EventStream::run` to return.
|
||||
drop(handle);
|
||||
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "stopped");
|
||||
assert_eq!(rx.recv_timeout(timeout()).unwrap(), "stopped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -500,11 +500,14 @@ mod tests {
|
||||
}
|
||||
|
||||
fn flush_historical_events() {
|
||||
let duration = if std::env::var("CI").is_ok() {
|
||||
Duration::from_secs(2)
|
||||
thread::sleep(timeout());
|
||||
}
|
||||
|
||||
fn timeout() -> Duration {
|
||||
if std::env::var("CI").is_ok() {
|
||||
Duration::from_secs(4)
|
||||
} else {
|
||||
Duration::from_millis(500)
|
||||
};
|
||||
thread::sleep(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2045,7 +2045,7 @@ fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
|
||||
OsString::from("status"),
|
||||
OsString::from("--porcelain=v1"),
|
||||
OsString::from("--untracked-files=all"),
|
||||
OsString::from("--no-renames"),
|
||||
OsString::from("--find-renames"),
|
||||
OsString::from("-z"),
|
||||
];
|
||||
args.extend(
|
||||
@@ -2387,22 +2387,37 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
|
||||
continue;
|
||||
}
|
||||
let mut fields = line.split('\x00');
|
||||
let is_current_branch = fields.next().context("no HEAD")? == "*";
|
||||
let head_sha: SharedString = fields.next().context("no objectname")?.to_string().into();
|
||||
let parent_sha: SharedString = fields.next().context("no parent")?.to_string().into();
|
||||
let ref_name = fields.next().context("no refname")?.to_string().into();
|
||||
let upstream_name = fields.next().context("no upstream")?.to_string();
|
||||
let upstream_tracking = parse_upstream_track(fields.next().context("no upstream:track")?)?;
|
||||
let commiterdate = fields.next().context("no committerdate")?.parse::<i64>()?;
|
||||
let author_name = fields.next().context("no authorname")?.to_string().into();
|
||||
let subject: SharedString = fields
|
||||
.next()
|
||||
.context("no contents:subject")?
|
||||
.to_string()
|
||||
.into();
|
||||
let Some(head) = fields.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(head_sha) = fields.next().map(|f| f.to_string().into()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(parent_sha) = fields.next().map(|f| f.to_string()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(ref_name) = fields.next().map(|f| f.to_string().into()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(upstream_name) = fields.next().map(|f| f.to_string()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(upstream_tracking) = fields.next().and_then(|f| parse_upstream_track(f).ok())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(commiterdate) = fields.next().and_then(|f| f.parse::<i64>().ok()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(author_name) = fields.next().map(|f| f.to_string().into()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(subject) = fields.next().map(|f| f.to_string().into()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
branches.push(Branch {
|
||||
is_head: is_current_branch,
|
||||
is_head: head == "*",
|
||||
ref_name,
|
||||
most_recent_commit: Some(CommitSummary {
|
||||
sha: head_sha,
|
||||
@@ -2744,6 +2759,44 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_branches_parsing_containing_refs_with_missing_fields() {
|
||||
#[allow(clippy::octal_escapes)]
|
||||
let input = " \090012116c03db04344ab10d50348553aa94f1ea0\0refs/heads/broken\n \0eb0cae33272689bd11030822939dd2701c52f81e\0895951d681e5561478c0acdd6905e8aacdfd2249\0refs/heads/dev\0\0\01762948725\0Zed\0Add feature\n*\0895951d681e5561478c0acdd6905e8aacdfd2249\0\0refs/heads/main\0\0\01762948695\0Zed\0Initial commit\n";
|
||||
|
||||
let branches = parse_branch_input(input).unwrap();
|
||||
assert_eq!(branches.len(), 2);
|
||||
assert_eq!(
|
||||
branches,
|
||||
vec![
|
||||
Branch {
|
||||
is_head: false,
|
||||
ref_name: "refs/heads/dev".into(),
|
||||
upstream: None,
|
||||
most_recent_commit: Some(CommitSummary {
|
||||
sha: "eb0cae33272689bd11030822939dd2701c52f81e".into(),
|
||||
subject: "Add feature".into(),
|
||||
commit_timestamp: 1762948725,
|
||||
author_name: SharedString::new("Zed"),
|
||||
has_parent: true,
|
||||
})
|
||||
},
|
||||
Branch {
|
||||
is_head: true,
|
||||
ref_name: "refs/heads/main".into(),
|
||||
upstream: None,
|
||||
most_recent_commit: Some(CommitSummary {
|
||||
sha: "895951d681e5561478c0acdd6905e8aacdfd2249".into(),
|
||||
subject: "Initial commit".into(),
|
||||
commit_timestamp: 1762948695,
|
||||
author_name: SharedString::new("Zed"),
|
||||
has_parent: false,
|
||||
})
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
impl RealGitRepository {
|
||||
/// Force a Git garbage collection on the repository.
|
||||
fn gc(&self) -> BoxFuture<'_, Result<()>> {
|
||||
|
||||
@@ -203,6 +203,14 @@ impl FileStatus {
|
||||
matches!(self, FileStatus::Untracked)
|
||||
}
|
||||
|
||||
pub fn is_renamed(self) -> bool {
|
||||
let FileStatus::Tracked(tracked) = self else {
|
||||
return false;
|
||||
};
|
||||
tracked.index_status == StatusCode::Renamed
|
||||
|| tracked.worktree_status == StatusCode::Renamed
|
||||
}
|
||||
|
||||
pub fn summary(self) -> GitSummary {
|
||||
match self {
|
||||
FileStatus::Ignored => GitSummary::UNCHANGED,
|
||||
@@ -430,34 +438,79 @@ impl std::ops::Sub for GitSummary {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GitStatus {
|
||||
pub entries: Arc<[(RepoPath, FileStatus)]>,
|
||||
pub renamed_paths: HashMap<RepoPath, RepoPath>,
|
||||
}
|
||||
|
||||
impl FromStr for GitStatus {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut entries = s
|
||||
.split('\0')
|
||||
.filter_map(|entry| {
|
||||
let sep = entry.get(2..3)?;
|
||||
if sep != " " {
|
||||
return None;
|
||||
let mut parts = s.split('\0').peekable();
|
||||
let mut entries = Vec::new();
|
||||
let mut renamed_paths = HashMap::default();
|
||||
|
||||
while let Some(entry) = parts.next() {
|
||||
if entry.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !matches!(entry.get(2..3), Some(" ")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path_or_old_path = &entry[3..];
|
||||
|
||||
if path_or_old_path.ends_with('/') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let status = match entry.as_bytes()[0..2].try_into() {
|
||||
Ok(bytes) => match FileStatus::from_bytes(bytes).log_err() {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
},
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let is_rename = matches!(
|
||||
status,
|
||||
FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Renamed | StatusCode::Copied,
|
||||
..
|
||||
}) | FileStatus::Tracked(TrackedStatus {
|
||||
worktree_status: StatusCode::Renamed | StatusCode::Copied,
|
||||
..
|
||||
})
|
||||
);
|
||||
|
||||
let (old_path_str, new_path_str) = if is_rename {
|
||||
let new_path = match parts.next() {
|
||||
Some(new_path) if !new_path.is_empty() => new_path,
|
||||
_ => continue,
|
||||
};
|
||||
let path = &entry[3..];
|
||||
// The git status output includes untracked directories as well as untracked files.
|
||||
// We do our own processing to compute the "summary" status of each directory,
|
||||
// so just skip any directories in the output, since they'll otherwise interfere
|
||||
// with our handling of nested repositories.
|
||||
if path.ends_with('/') {
|
||||
return None;
|
||||
(path_or_old_path, new_path)
|
||||
} else {
|
||||
(path_or_old_path, path_or_old_path)
|
||||
};
|
||||
|
||||
if new_path_str.ends_with('/') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_path = match RelPath::unix(new_path_str).log_err() {
|
||||
Some(p) => RepoPath::from_rel_path(p),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if is_rename {
|
||||
if let Some(old_path_rel) = RelPath::unix(old_path_str).log_err() {
|
||||
let old_path_repo = RepoPath::from_rel_path(old_path_rel);
|
||||
renamed_paths.insert(new_path.clone(), old_path_repo);
|
||||
}
|
||||
let status = entry.as_bytes()[0..2].try_into().unwrap();
|
||||
let status = FileStatus::from_bytes(status).log_err()?;
|
||||
// git-status outputs `/`-delimited repo paths, even on Windows.
|
||||
let path = RepoPath::from_rel_path(RelPath::unix(path).log_err()?);
|
||||
Some((path, status))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
entries.push((new_path, status));
|
||||
}
|
||||
entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
|
||||
// When a file exists in HEAD, is deleted in the index, and exists again in the working copy,
|
||||
// git produces two lines for it, one reading `D ` (deleted in index, unmodified in working copy)
|
||||
@@ -481,6 +534,7 @@ impl FromStr for GitStatus {
|
||||
});
|
||||
Ok(Self {
|
||||
entries: entries.into(),
|
||||
renamed_paths,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -489,6 +543,7 @@ impl Default for GitStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
entries: Arc::new([]),
|
||||
renamed_paths: HashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ use agent_settings::AgentSettings;
|
||||
use anyhow::Context as _;
|
||||
use askpass::AskPassDelegate;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
|
||||
use editor::{
|
||||
Direction, Editor, EditorElement, EditorMode, MultiBuffer, actions::ExpandAllDiffHunks,
|
||||
};
|
||||
use futures::StreamExt as _;
|
||||
use git::blame::ParsedCommitMessage;
|
||||
use git::repository::{
|
||||
@@ -69,7 +71,7 @@ use cloud_llm_client::CompletionIntent;
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId},
|
||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt},
|
||||
};
|
||||
|
||||
actions!(
|
||||
@@ -409,17 +411,10 @@ impl GitPanel {
|
||||
}
|
||||
GitStoreEvent::RepositoryUpdated(
|
||||
_,
|
||||
RepositoryEvent::StatusesChanged { full_scan: true }
|
||||
RepositoryEvent::StatusesChanged
|
||||
| RepositoryEvent::BranchChanged
|
||||
| RepositoryEvent::MergeHeadsChanged,
|
||||
true,
|
||||
) => {
|
||||
this.schedule_update(window, cx);
|
||||
}
|
||||
GitStoreEvent::RepositoryUpdated(
|
||||
_,
|
||||
RepositoryEvent::StatusesChanged { full_scan: false },
|
||||
true,
|
||||
)
|
||||
| GitStoreEvent::RepositoryAdded
|
||||
| GitStoreEvent::RepositoryRemoved(_) => {
|
||||
@@ -745,7 +740,7 @@ impl GitPanel {
|
||||
) {
|
||||
self.select_first_entry_if_none(cx);
|
||||
|
||||
cx.focus_self(window);
|
||||
self.focus_handle.focus(window);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -799,15 +794,46 @@ impl GitPanel {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.workspace
|
||||
let open_task = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_path_preview(path, None, false, false, true, window, cx)
|
||||
.detach_and_prompt_err("Failed to open file", window, cx, |e, _, _| {
|
||||
Some(format!("{e}"))
|
||||
});
|
||||
workspace.open_path_preview(path, None, false, false, true, window, cx)
|
||||
})
|
||||
.ok()
|
||||
.ok()?;
|
||||
|
||||
cx.spawn_in(window, async move |_, mut cx| {
|
||||
let item = open_task
|
||||
.await
|
||||
.notify_async_err(&mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to open file"))?;
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
if let Some(diff_task) =
|
||||
active_editor.update(cx, |editor, _cx| editor.wait_for_diff_to_load())?
|
||||
{
|
||||
diff_task.await;
|
||||
}
|
||||
|
||||
cx.update(|window, cx| {
|
||||
active_editor.update(cx, |editor, cx| {
|
||||
editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
|
||||
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
language::Point::new(0, 0),
|
||||
Direction::Next,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1224,14 +1250,18 @@ impl GitPanel {
|
||||
let Some(active_repository) = self.active_repository.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let repo = active_repository.read(cx);
|
||||
let (stage, repo_paths) = match entry {
|
||||
GitListEntry::Status(status_entry) => {
|
||||
let repo_paths = vec![status_entry.clone()];
|
||||
let stage = if active_repository
|
||||
.read(cx)
|
||||
let stage = if repo
|
||||
.pending_ops_for_path(&status_entry.repo_path)
|
||||
.map(|ops| ops.staging() || ops.staged())
|
||||
.unwrap_or(status_entry.status.staging().has_staged())
|
||||
.or_else(|| {
|
||||
repo.status_for_path(&status_entry.repo_path)
|
||||
.map(|status| status.status.staging().has_staged())
|
||||
})
|
||||
.unwrap_or(status_entry.staging.has_staged())
|
||||
{
|
||||
if let Some(op) = self.bulk_staging.clone()
|
||||
&& op.anchor == status_entry.repo_path
|
||||
@@ -1247,13 +1277,12 @@ impl GitPanel {
|
||||
}
|
||||
GitListEntry::Header(section) => {
|
||||
let goal_staged_state = !self.header_state(section.header).selected();
|
||||
let repository = active_repository.read(cx);
|
||||
let entries = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.status_entry())
|
||||
.filter(|status_entry| {
|
||||
section.contains(status_entry, repository)
|
||||
section.contains(status_entry, repo)
|
||||
&& status_entry.staging.as_bool() != Some(goal_staged_state)
|
||||
})
|
||||
.cloned()
|
||||
@@ -3224,18 +3253,12 @@ impl GitPanel {
|
||||
) -> Option<impl IntoElement> {
|
||||
self.active_repository.as_ref()?;
|
||||
|
||||
let text;
|
||||
let action;
|
||||
let tooltip;
|
||||
if self.total_staged_count() == self.entry_count && self.entry_count > 0 {
|
||||
text = "Unstage All";
|
||||
action = git::UnstageAll.boxed_clone();
|
||||
tooltip = "git reset";
|
||||
} else {
|
||||
text = "Stage All";
|
||||
action = git::StageAll.boxed_clone();
|
||||
tooltip = "git add --all ."
|
||||
}
|
||||
let (text, action, stage, tooltip) =
|
||||
if self.total_staged_count() == self.entry_count && self.entry_count > 0 {
|
||||
("Unstage All", UnstageAll.boxed_clone(), false, "git reset")
|
||||
} else {
|
||||
("Stage All", StageAll.boxed_clone(), true, "git add --all")
|
||||
};
|
||||
|
||||
let change_string = match self.entry_count {
|
||||
0 => "No Changes".to_string(),
|
||||
@@ -3273,11 +3296,15 @@ impl GitPanel {
|
||||
&self.focus_handle,
|
||||
))
|
||||
.disabled(self.entry_count == 0)
|
||||
.on_click(move |_, _, cx| {
|
||||
let action = action.boxed_clone();
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(action.as_ref());
|
||||
})
|
||||
.on_click({
|
||||
let git_panel = cx.weak_entity();
|
||||
move |_, _, cx| {
|
||||
git_panel
|
||||
.update(cx, |git_panel, cx| {
|
||||
git_panel.change_all_files_stage(stage, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -3661,13 +3688,18 @@ impl GitPanel {
|
||||
let ix = self.entry_by_path(&repo_path, cx)?;
|
||||
let entry = self.entries.get(ix)?;
|
||||
|
||||
let is_staging_or_staged = if let Some(status_entry) = entry.status_entry() {
|
||||
repo.pending_ops_for_path(&repo_path)
|
||||
.map(|ops| ops.staging() || ops.staged())
|
||||
.unwrap_or(status_entry.staging.has_staged())
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let is_staging_or_staged = repo
|
||||
.pending_ops_for_path(&repo_path)
|
||||
.map(|ops| ops.staging() || ops.staged())
|
||||
.or_else(|| {
|
||||
repo.status_for_path(&repo_path)
|
||||
.and_then(|status| status.status.staging().as_bool())
|
||||
})
|
||||
.or_else(|| {
|
||||
entry
|
||||
.status_entry()
|
||||
.and_then(|entry| entry.staging.as_bool())
|
||||
});
|
||||
|
||||
let checkbox = Checkbox::new("stage-file", is_staging_or_staged.into())
|
||||
.disabled(!self.has_write_access(cx))
|
||||
@@ -3925,6 +3957,20 @@ impl GitPanel {
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
let display_name = entry.display_name(path_style);
|
||||
|
||||
let active_repo = self
|
||||
.project
|
||||
.read(cx)
|
||||
.active_repository(cx)
|
||||
.expect("active repository must be set");
|
||||
let repo = active_repo.read(cx);
|
||||
let repo_snapshot = repo.snapshot();
|
||||
|
||||
let old_path = if entry.status.is_renamed() {
|
||||
repo_snapshot.renamed_paths.get(&entry.repo_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let selected = self.selected_entry == Some(ix);
|
||||
let marked = self.marked_entries.contains(&ix);
|
||||
let status_style = GitPanelSettings::get_global(cx).status_style;
|
||||
@@ -3933,15 +3979,16 @@ impl GitPanel {
|
||||
let has_conflict = status.is_conflicted();
|
||||
let is_modified = status.is_modified();
|
||||
let is_deleted = status.is_deleted();
|
||||
let is_renamed = status.is_renamed();
|
||||
|
||||
let label_color = if status_style == StatusStyle::LabelColor {
|
||||
if has_conflict {
|
||||
Color::VersionControlConflict
|
||||
} else if is_modified {
|
||||
Color::VersionControlModified
|
||||
} else if is_deleted {
|
||||
// We don't want a bunch of red labels in the list
|
||||
Color::Disabled
|
||||
} else if is_renamed || is_modified {
|
||||
Color::VersionControlModified
|
||||
} else {
|
||||
Color::VersionControlAdded
|
||||
}
|
||||
@@ -3961,12 +4008,6 @@ impl GitPanel {
|
||||
let checkbox_id: ElementId =
|
||||
ElementId::Name(format!("entry_{}_{}_checkbox", display_name, ix).into());
|
||||
|
||||
let active_repo = self
|
||||
.project
|
||||
.read(cx)
|
||||
.active_repository(cx)
|
||||
.expect("active repository must be set");
|
||||
let repo = active_repo.read(cx);
|
||||
// Checking for current staged/unstaged file status is a chained operation:
|
||||
// 1. first, we check for any pending operation recorded in repository
|
||||
// 2. if there are no pending ops either running or finished, we then ask the repository
|
||||
@@ -4121,23 +4162,32 @@ impl GitPanel {
|
||||
.items_center()
|
||||
.flex_1()
|
||||
// .overflow_hidden()
|
||||
.when_some(entry.parent_dir(path_style), |this, parent| {
|
||||
if !parent.is_empty() {
|
||||
this.child(
|
||||
self.entry_label(
|
||||
format!("{parent}{}", path_style.separator()),
|
||||
path_color,
|
||||
)
|
||||
.when(status.is_deleted(), |this| this.strikethrough()),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
.when_some(old_path.as_ref(), |this, old_path| {
|
||||
let new_display = old_path.display(path_style).to_string();
|
||||
let old_display = entry.repo_path.display(path_style).to_string();
|
||||
this.child(self.entry_label(old_display, Color::Muted).strikethrough())
|
||||
.child(self.entry_label(" → ", Color::Muted))
|
||||
.child(self.entry_label(new_display, label_color))
|
||||
})
|
||||
.child(
|
||||
self.entry_label(display_name, label_color)
|
||||
.when(status.is_deleted(), |this| this.strikethrough()),
|
||||
),
|
||||
.when(old_path.is_none(), |this| {
|
||||
this.when_some(entry.parent_dir(path_style), |this, parent| {
|
||||
if !parent.is_empty() {
|
||||
this.child(
|
||||
self.entry_label(
|
||||
format!("{parent}{}", path_style.separator()),
|
||||
path_color,
|
||||
)
|
||||
.when(status.is_deleted(), |this| this.strikethrough()),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.child(
|
||||
self.entry_label(display_name, label_color)
|
||||
.when(status.is_deleted(), |this| this.strikethrough()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -708,6 +708,11 @@ impl RenderOnce for GitStatusIcon {
|
||||
IconName::SquareMinus,
|
||||
cx.theme().colors().version_control_deleted,
|
||||
)
|
||||
} else if status.is_renamed() {
|
||||
(
|
||||
IconName::ArrowRight,
|
||||
cx.theme().colors().version_control_modified,
|
||||
)
|
||||
} else if status.is_modified() {
|
||||
(
|
||||
IconName::SquareDot,
|
||||
|
||||
@@ -229,6 +229,10 @@ pub struct GenerativeContentBlob {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FunctionCallPart {
|
||||
pub function_call: FunctionCall,
|
||||
/// Thought signature returned by the model for function calls.
|
||||
/// Only present on the first function call in parallel call scenarios.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub thought_signature: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -636,3 +640,109 @@ impl std::fmt::Display for Model {
|
||||
write!(f, "{}", self.id())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_function_call_part_with_signature_serializes_correctly() {
|
||||
let part = FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "test_function".to_string(),
|
||||
args: json!({"arg": "value"}),
|
||||
},
|
||||
thought_signature: Some("test_signature".to_string()),
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(&part).unwrap();
|
||||
|
||||
assert_eq!(serialized["functionCall"]["name"], "test_function");
|
||||
assert_eq!(serialized["functionCall"]["args"]["arg"], "value");
|
||||
assert_eq!(serialized["thoughtSignature"], "test_signature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_call_part_without_signature_omits_field() {
|
||||
let part = FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "test_function".to_string(),
|
||||
args: json!({"arg": "value"}),
|
||||
},
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(&part).unwrap();
|
||||
|
||||
assert_eq!(serialized["functionCall"]["name"], "test_function");
|
||||
assert_eq!(serialized["functionCall"]["args"]["arg"], "value");
|
||||
// thoughtSignature field should not be present when None
|
||||
assert!(serialized.get("thoughtSignature").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_call_part_deserializes_with_signature() {
|
||||
let json = json!({
|
||||
"functionCall": {
|
||||
"name": "test_function",
|
||||
"args": {"arg": "value"}
|
||||
},
|
||||
"thoughtSignature": "test_signature"
|
||||
});
|
||||
|
||||
let part: FunctionCallPart = serde_json::from_value(json).unwrap();
|
||||
|
||||
assert_eq!(part.function_call.name, "test_function");
|
||||
assert_eq!(part.thought_signature, Some("test_signature".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_call_part_deserializes_without_signature() {
|
||||
let json = json!({
|
||||
"functionCall": {
|
||||
"name": "test_function",
|
||||
"args": {"arg": "value"}
|
||||
}
|
||||
});
|
||||
|
||||
let part: FunctionCallPart = serde_json::from_value(json).unwrap();
|
||||
|
||||
assert_eq!(part.function_call.name, "test_function");
|
||||
assert_eq!(part.thought_signature, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_call_part_round_trip() {
|
||||
let original = FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "test_function".to_string(),
|
||||
args: json!({"arg": "value", "nested": {"key": "val"}}),
|
||||
},
|
||||
thought_signature: Some("round_trip_signature".to_string()),
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(&original).unwrap();
|
||||
let deserialized: FunctionCallPart = serde_json::from_value(serialized).unwrap();
|
||||
|
||||
assert_eq!(deserialized.function_call.name, original.function_call.name);
|
||||
assert_eq!(deserialized.function_call.args, original.function_call.args);
|
||||
assert_eq!(deserialized.thought_signature, original.thought_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_call_part_with_empty_signature_serializes() {
|
||||
let part = FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "test_function".to_string(),
|
||||
args: json!({"arg": "value"}),
|
||||
},
|
||||
thought_signature: Some("".to_string()),
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(&part).unwrap();
|
||||
|
||||
// Empty string should still be serialized (normalization happens at a higher level)
|
||||
assert_eq!(serialized["thoughtSignature"], "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,4 +63,4 @@ In addition to the systems above, GPUI provides a range of smaller services that
|
||||
|
||||
- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details.
|
||||
|
||||
Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
|
||||
Currently, the best way to learn about these APIs is to read the Zed source code or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
|
||||
|
||||
@@ -2400,10 +2400,6 @@ impl HttpClient for NullHttpClient {
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
/// A mutable reference to an entity owned by GPUI
|
||||
|
||||
@@ -509,7 +509,7 @@ impl Deref for MouseExitEvent {
|
||||
}
|
||||
|
||||
/// A collection of paths from the platform, such as from a file drop.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
|
||||
|
||||
impl ExternalPaths {
|
||||
|
||||
@@ -1573,6 +1573,8 @@ pub enum ClipboardEntry {
|
||||
String(ClipboardString),
|
||||
/// An image entry
|
||||
Image(Image),
|
||||
/// A file entry
|
||||
ExternalPaths(crate::ExternalPaths),
|
||||
}
|
||||
|
||||
impl ClipboardItem {
|
||||
@@ -1613,16 +1615,29 @@ impl ClipboardItem {
|
||||
/// Returns None if there were no ClipboardString entries.
|
||||
pub fn text(&self) -> Option<String> {
|
||||
let mut answer = String::new();
|
||||
let mut any_entries = false;
|
||||
|
||||
for entry in self.entries.iter() {
|
||||
if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
|
||||
answer.push_str(text);
|
||||
any_entries = true;
|
||||
}
|
||||
}
|
||||
|
||||
if any_entries { Some(answer) } else { None }
|
||||
if answer.is_empty() {
|
||||
for entry in self.entries.iter() {
|
||||
if let ClipboardEntry::ExternalPaths(paths) = entry {
|
||||
for path in &paths.0 {
|
||||
use std::fmt::Write as _;
|
||||
_ = write!(answer, "{}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !answer.is_empty() {
|
||||
Some(answer)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// If this item is one ClipboardEntry::String, returns its metadata.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -18,6 +17,7 @@ use anyhow::{Context as _, anyhow};
|
||||
use calloop::{LoopSignal, channel::Channel};
|
||||
use futures::channel::oneshot;
|
||||
use util::ResultExt as _;
|
||||
use util::command::{new_smol_command, new_std_command};
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||
|
||||
@@ -215,7 +215,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
clippy::disallowed_methods,
|
||||
reason = "We are restarting ourselves, using std command thus is fine"
|
||||
)]
|
||||
let restart_process = Command::new("/usr/bin/env")
|
||||
let restart_process = new_std_command("/usr/bin/env")
|
||||
.arg("bash")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
@@ -422,7 +422,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
let path = path.to_owned();
|
||||
self.background_executor()
|
||||
.spawn(async move {
|
||||
let _ = smol::process::Command::new("xdg-open")
|
||||
let _ = new_smol_command("xdg-open")
|
||||
.arg(path)
|
||||
.spawn()
|
||||
.context("invoking xdg-open")
|
||||
|
||||
@@ -53,14 +53,16 @@ use std::{
|
||||
ffi::{CStr, OsStr, c_void},
|
||||
os::{raw::c_char, unix::ffi::OsStrExt},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
ptr,
|
||||
rc::Rc,
|
||||
slice, str,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use strum::IntoEnumIterator;
|
||||
use util::ResultExt;
|
||||
use util::{
|
||||
ResultExt,
|
||||
command::{new_smol_command, new_std_command},
|
||||
};
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSUTF8StringEncoding: NSUInteger = 4;
|
||||
@@ -552,7 +554,7 @@ impl Platform for MacPlatform {
|
||||
clippy::disallowed_methods,
|
||||
reason = "We are restarting ourselves, using std command thus is fine"
|
||||
)]
|
||||
let restart_process = Command::new("/bin/bash")
|
||||
let restart_process = new_std_command("/bin/bash")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
.arg(app_pid)
|
||||
@@ -867,7 +869,7 @@ impl Platform for MacPlatform {
|
||||
.lock()
|
||||
.background_executor
|
||||
.spawn(async move {
|
||||
if let Some(mut child) = smol::process::Command::new("open")
|
||||
if let Some(mut child) = new_smol_command("open")
|
||||
.arg(path)
|
||||
.spawn()
|
||||
.context("invoking open command")
|
||||
@@ -1046,6 +1048,7 @@ impl Platform for MacPlatform {
|
||||
ClipboardEntry::Image(image) => {
|
||||
self.write_image_to_clipboard(image);
|
||||
}
|
||||
ClipboardEntry::ExternalPaths(_) => {}
|
||||
},
|
||||
None => {
|
||||
// Writing an empty list of entries just clears the clipboard.
|
||||
|
||||
@@ -1543,6 +1543,17 @@ impl PlatformWindow for MacWindow {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn start_window_move(&self) {
|
||||
let this = self.0.lock();
|
||||
let window = this.native_window;
|
||||
|
||||
unsafe {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let mut event: id = msg_send![app, currentEvent];
|
||||
let _: () = msg_send![window, performWindowDragWithEvent: event];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for MacWindow {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{FxHashMap, FxHashSet};
|
||||
use collections::FxHashMap;
|
||||
use itertools::Itertools;
|
||||
use windows::Win32::{
|
||||
Foundation::{HANDLE, HGLOBAL},
|
||||
@@ -18,7 +18,9 @@ use windows::Win32::{
|
||||
};
|
||||
use windows_core::PCWSTR;
|
||||
|
||||
use crate::{ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, hash};
|
||||
use crate::{
|
||||
ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash,
|
||||
};
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
||||
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
|
||||
@@ -48,16 +50,6 @@ static FORMATS_MAP: LazyLock<FxHashMap<u32, ClipboardFormatType>> = LazyLock::ne
|
||||
formats_map.insert(CF_HDROP.0 as u32, ClipboardFormatType::Files);
|
||||
formats_map
|
||||
});
|
||||
static FORMATS_SET: LazyLock<FxHashSet<u32>> = LazyLock::new(|| {
|
||||
let mut formats_map = FxHashSet::default();
|
||||
formats_map.insert(CF_UNICODETEXT.0 as u32);
|
||||
formats_map.insert(*CLIPBOARD_PNG_FORMAT);
|
||||
formats_map.insert(*CLIPBOARD_GIF_FORMAT);
|
||||
formats_map.insert(*CLIPBOARD_JPG_FORMAT);
|
||||
formats_map.insert(*CLIPBOARD_SVG_FORMAT);
|
||||
formats_map.insert(CF_HDROP.0 as u32);
|
||||
formats_map
|
||||
});
|
||||
static IMAGE_FORMATS_MAP: LazyLock<FxHashMap<u32, ImageFormat>> = LazyLock::new(|| {
|
||||
let mut formats_map = FxHashMap::default();
|
||||
formats_map.insert(*CLIPBOARD_PNG_FORMAT, ImageFormat::Png);
|
||||
@@ -138,6 +130,11 @@ fn register_clipboard_format(format: PCWSTR) -> u32 {
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
log::debug!(
|
||||
"Registered clipboard format {} as {}",
|
||||
unsafe { format.display() },
|
||||
ret
|
||||
);
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -159,6 +156,7 @@ fn write_to_clipboard_inner(item: ClipboardItem) -> Result<()> {
|
||||
ClipboardEntry::Image(image) => {
|
||||
write_image_to_clipboard(image)?;
|
||||
}
|
||||
ClipboardEntry::ExternalPaths(_) => {}
|
||||
},
|
||||
None => {
|
||||
// Writing an empty list of entries just clears the clipboard.
|
||||
@@ -249,19 +247,33 @@ fn with_best_match_format<F>(f: F) -> Option<ClipboardItem>
|
||||
where
|
||||
F: Fn(u32) -> Option<ClipboardEntry>,
|
||||
{
|
||||
let mut text = None;
|
||||
let mut image = None;
|
||||
let mut files = None;
|
||||
let count = unsafe { CountClipboardFormats() };
|
||||
let mut clipboard_format = 0;
|
||||
for _ in 0..count {
|
||||
clipboard_format = unsafe { EnumClipboardFormats(clipboard_format) };
|
||||
let Some(item_format) = FORMATS_SET.get(&clipboard_format) else {
|
||||
let Some(item_format) = FORMATS_MAP.get(&clipboard_format) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(entry) = f(*item_format) {
|
||||
return Some(ClipboardItem {
|
||||
entries: vec![entry],
|
||||
});
|
||||
let bucket = match item_format {
|
||||
ClipboardFormatType::Text if text.is_none() => &mut text,
|
||||
ClipboardFormatType::Image if image.is_none() => &mut image,
|
||||
ClipboardFormatType::Files if files.is_none() => &mut files,
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(entry) = f(clipboard_format) {
|
||||
*bucket = Some(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(entry) = [image, files, text].into_iter().flatten().next() {
|
||||
return Some(ClipboardItem {
|
||||
entries: vec![entry],
|
||||
});
|
||||
}
|
||||
|
||||
// log the formats that we don't support yet.
|
||||
{
|
||||
clipboard_format = 0;
|
||||
@@ -346,18 +358,17 @@ fn read_image_for_type(format_number: u32, format: ImageFormat) -> Option<Clipbo
|
||||
}
|
||||
|
||||
fn read_files_from_clipboard() -> Option<ClipboardEntry> {
|
||||
let text = with_clipboard_data(CF_HDROP.0 as u32, |data_ptr, _size| {
|
||||
let filenames = with_clipboard_data(CF_HDROP.0 as u32, |data_ptr, _size| {
|
||||
let hdrop = HDROP(data_ptr);
|
||||
let mut filenames = String::new();
|
||||
let mut filenames = Vec::new();
|
||||
with_file_names(hdrop, |file_name| {
|
||||
filenames.push_str(&file_name);
|
||||
filenames.push(std::path::PathBuf::from(file_name));
|
||||
});
|
||||
filenames
|
||||
})?;
|
||||
Some(ClipboardEntry::String(ClipboardString {
|
||||
text,
|
||||
metadata: None,
|
||||
}))
|
||||
Some(ClipboardEntry::ExternalPaths(ExternalPaths(
|
||||
filenames.into(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn with_clipboard_data<F, R>(format: u32, f: F) -> Option<R>
|
||||
|
||||
@@ -234,11 +234,14 @@ impl DirectXAtlasState {
|
||||
}
|
||||
|
||||
fn texture(&self, id: AtlasTextureId) -> &DirectXAtlasTexture {
|
||||
let textures = match id.kind {
|
||||
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
|
||||
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
|
||||
};
|
||||
textures[id.index as usize].as_ref().unwrap()
|
||||
match id.kind {
|
||||
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures[id.index as usize]
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures[id.index as usize]
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ pub(crate) struct DirectXRenderer {
|
||||
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
/// Whether we want to skip drwaing due to device lost events.
|
||||
///
|
||||
/// In that case we want to discard the first frame that we draw as we got reset in the middle of a frame
|
||||
/// meaning we lost all the allocated gpu textures and scene resources.
|
||||
skip_draws: bool,
|
||||
}
|
||||
|
||||
/// Direct3D objects
|
||||
@@ -167,6 +173,7 @@ impl DirectXRenderer {
|
||||
font_info: Self::get_font_info(),
|
||||
width: 1,
|
||||
height: 1,
|
||||
skip_draws: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,8 +199,13 @@ impl DirectXRenderer {
|
||||
}],
|
||||
)?;
|
||||
unsafe {
|
||||
device_context
|
||||
.ClearRenderTargetView(resources.render_target_view.as_ref().unwrap(), &[0.0; 4]);
|
||||
device_context.ClearRenderTargetView(
|
||||
resources
|
||||
.render_target_view
|
||||
.as_ref()
|
||||
.context("missing render target view")?,
|
||||
&[0.0; 4],
|
||||
);
|
||||
device_context
|
||||
.OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None);
|
||||
device_context.RSSetViewports(Some(slice::from_ref(&resources.viewport)));
|
||||
@@ -283,10 +295,16 @@ impl DirectXRenderer {
|
||||
self.globals = globals;
|
||||
self.pipelines = pipelines;
|
||||
self.direct_composition = direct_composition;
|
||||
self.skip_draws = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn draw(&mut self, scene: &Scene) -> Result<()> {
|
||||
if self.skip_draws {
|
||||
// skip drawing this frame, we just recovered from a device lost event
|
||||
// and so likely do not have the textures anymore that are required for drawing
|
||||
return Ok(());
|
||||
}
|
||||
self.pre_draw()?;
|
||||
for batch in scene.batches() {
|
||||
match batch {
|
||||
@@ -306,14 +324,18 @@ impl DirectXRenderer {
|
||||
sprites,
|
||||
} => self.draw_polychrome_sprites(texture_id, sprites),
|
||||
PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(surfaces),
|
||||
}.context(format!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
|
||||
scene.paths.len(),
|
||||
scene.shadows.len(),
|
||||
scene.quads.len(),
|
||||
scene.underlines.len(),
|
||||
scene.monochrome_sprites.len(),
|
||||
scene.polychrome_sprites.len(),
|
||||
scene.surfaces.len(),))?;
|
||||
}
|
||||
.context(format!(
|
||||
"scene too large:\
|
||||
{} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
|
||||
scene.paths.len(),
|
||||
scene.shadows.len(),
|
||||
scene.quads.len(),
|
||||
scene.underlines.len(),
|
||||
scene.monochrome_sprites.len(),
|
||||
scene.polychrome_sprites.len(),
|
||||
scene.surfaces.len(),
|
||||
))?;
|
||||
}
|
||||
self.present()
|
||||
}
|
||||
@@ -352,6 +374,7 @@ impl DirectXRenderer {
|
||||
}
|
||||
|
||||
resources.recreate_resources(devices, width, height)?;
|
||||
|
||||
unsafe {
|
||||
devices
|
||||
.device_context
|
||||
@@ -647,6 +670,10 @@ impl DirectXRenderer {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn mark_drawable(&mut self) {
|
||||
self.skip_draws = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectXResources {
|
||||
|
||||
@@ -201,8 +201,10 @@ impl WindowsWindowInner {
|
||||
let new_logical_size = device_size.to_pixels(scale_factor);
|
||||
let mut lock = self.state.borrow_mut();
|
||||
lock.logical_size = new_logical_size;
|
||||
if should_resize_renderer {
|
||||
lock.renderer.resize(device_size).log_err();
|
||||
if should_resize_renderer && let Err(e) = lock.renderer.resize(device_size) {
|
||||
log::error!("Failed to resize renderer, invalidating devices: {}", e);
|
||||
lock.invalidate_devices
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
if let Some(mut callback) = lock.callbacks.resize.take() {
|
||||
drop(lock);
|
||||
@@ -1138,6 +1140,11 @@ impl WindowsWindowInner {
|
||||
#[inline]
|
||||
fn draw_window(&self, handle: HWND, force_render: bool) -> Option<isize> {
|
||||
let mut request_frame = self.state.borrow_mut().callbacks.request_frame.take()?;
|
||||
|
||||
// we are instructing gpui to force render a frame, this will
|
||||
// re-populate all the gpu textures for us so we can resume drawing in
|
||||
// case we disabled drawing earlier due to a device loss
|
||||
self.state.borrow_mut().renderer.mark_drawable();
|
||||
request_frame(RequestFrameOptions {
|
||||
require_presentation: false,
|
||||
force_render,
|
||||
|
||||
@@ -3,7 +3,10 @@ use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
rc::{Rc, Weak},
|
||||
sync::{Arc, atomic::Ordering},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use ::util::{ResultExt, paths::SanitizedPath};
|
||||
@@ -36,6 +39,9 @@ pub(crate) struct WindowsPlatform {
|
||||
text_system: Arc<DirectWriteTextSystem>,
|
||||
windows_version: WindowsVersion,
|
||||
drop_target_helper: IDropTargetHelper,
|
||||
/// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices
|
||||
/// as resizing them has failed, causing us to have lost at least the render target.
|
||||
invalidate_devices: Arc<AtomicBool>,
|
||||
handle: HWND,
|
||||
disable_direct_composition: bool,
|
||||
}
|
||||
@@ -162,6 +168,7 @@ impl WindowsPlatform {
|
||||
disable_direct_composition,
|
||||
windows_version,
|
||||
drop_target_helper,
|
||||
invalidate_devices: Arc::new(AtomicBool::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,6 +202,7 @@ impl WindowsPlatform {
|
||||
platform_window_handle: self.handle,
|
||||
disable_direct_composition: self.disable_direct_composition,
|
||||
directx_devices: self.inner.state.borrow().directx_devices.clone().unwrap(),
|
||||
invalidate_devices: self.invalidate_devices.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,13 +255,17 @@ impl WindowsPlatform {
|
||||
let validation_number = self.inner.validation_number;
|
||||
let all_windows = Arc::downgrade(&self.raw_window_handles);
|
||||
let text_system = Arc::downgrade(&self.text_system);
|
||||
let invalidate_devices = self.invalidate_devices.clone();
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("VSyncProvider".to_owned())
|
||||
.spawn(move || {
|
||||
let vsync_provider = VSyncProvider::new();
|
||||
loop {
|
||||
vsync_provider.wait_for_vsync();
|
||||
if check_device_lost(&directx_device.device) {
|
||||
if check_device_lost(&directx_device.device)
|
||||
|| invalidate_devices.fetch_and(false, Ordering::Acquire)
|
||||
{
|
||||
if let Err(err) = handle_gpu_device_lost(
|
||||
&mut directx_device,
|
||||
platform_window.as_raw(),
|
||||
@@ -877,6 +889,9 @@ pub(crate) struct WindowCreationInfo {
|
||||
pub(crate) platform_window_handle: HWND,
|
||||
pub(crate) disable_direct_composition: bool,
|
||||
pub(crate) directx_devices: DirectXDevices,
|
||||
/// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices
|
||||
/// as resizing them has failed, causing us to have lost at least the render target.
|
||||
pub(crate) invalidate_devices: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
struct PlatformWindowCreateContext {
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
rc::{Rc, Weak},
|
||||
str::FromStr,
|
||||
sync::{Arc, Once},
|
||||
sync::{Arc, Once, atomic::AtomicBool},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
@@ -53,6 +53,9 @@ pub struct WindowsWindowState {
|
||||
pub nc_button_pressed: Option<u32>,
|
||||
|
||||
pub display: WindowsDisplay,
|
||||
/// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices
|
||||
/// as resizing them has failed, causing us to have lost at least the render target.
|
||||
pub invalidate_devices: Arc<AtomicBool>,
|
||||
fullscreen: Option<StyleAndBounds>,
|
||||
initial_placement: Option<WindowOpenStatus>,
|
||||
hwnd: HWND,
|
||||
@@ -83,6 +86,7 @@ impl WindowsWindowState {
|
||||
min_size: Option<Size<Pixels>>,
|
||||
appearance: WindowAppearance,
|
||||
disable_direct_composition: bool,
|
||||
invalidate_devices: Arc<AtomicBool>,
|
||||
) -> Result<Self> {
|
||||
let scale_factor = {
|
||||
let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
|
||||
@@ -138,6 +142,7 @@ impl WindowsWindowState {
|
||||
fullscreen,
|
||||
initial_placement,
|
||||
hwnd,
|
||||
invalidate_devices,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -211,6 +216,7 @@ impl WindowsWindowInner {
|
||||
context.min_size,
|
||||
context.appearance,
|
||||
context.disable_direct_composition,
|
||||
context.invalidate_devices.clone(),
|
||||
)?);
|
||||
|
||||
Ok(Rc::new(Self {
|
||||
@@ -361,6 +367,7 @@ struct WindowCreateContext {
|
||||
appearance: WindowAppearance,
|
||||
disable_direct_composition: bool,
|
||||
directx_devices: DirectXDevices,
|
||||
invalidate_devices: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl WindowsWindow {
|
||||
@@ -380,6 +387,7 @@ impl WindowsWindow {
|
||||
platform_window_handle,
|
||||
disable_direct_composition,
|
||||
directx_devices,
|
||||
invalidate_devices,
|
||||
} = creation_info;
|
||||
register_window_class(icon);
|
||||
let hide_title_bar = params
|
||||
@@ -440,6 +448,7 @@ impl WindowsWindow {
|
||||
appearance,
|
||||
disable_direct_composition,
|
||||
directx_devices,
|
||||
invalidate_devices,
|
||||
};
|
||||
let creation_result = unsafe {
|
||||
CreateWindowExW(
|
||||
|
||||
@@ -13,8 +13,9 @@ const ELLIPSIS: SharedString = SharedString::new_static("…");
|
||||
|
||||
/// A trait for elements that can be styled.
|
||||
/// Use this to opt-in to a utility CSS-like styling API.
|
||||
// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv
|
||||
#[cfg_attr(
|
||||
any(feature = "inspector", debug_assertions),
|
||||
all(any(feature = "inspector", debug_assertions), not(rust_analyzer)),
|
||||
gpui_macros::derive_inspector_reflection
|
||||
)]
|
||||
pub trait Styled: Sized {
|
||||
|
||||
@@ -1819,6 +1819,7 @@ impl Window {
|
||||
self.platform_window.show_window_menu(position)
|
||||
}
|
||||
|
||||
/// Handle window movement for Linux and macOS.
|
||||
/// Tells the compositor to take control of window movement (Wayland and X11)
|
||||
///
|
||||
/// Events may not be received during a move operation.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! This code was generated using Zed Agent with Claude Opus 4.
|
||||
|
||||
use gpui_macros::derive_inspector_reflection;
|
||||
|
||||
#[derive_inspector_reflection]
|
||||
// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv
|
||||
#[cfg_attr(not(rust_analyzer), gpui_macros::derive_inspector_reflection)]
|
||||
trait Transform: Clone {
|
||||
/// Doubles the value
|
||||
fn double(self) -> Self;
|
||||
|
||||
@@ -14,9 +14,9 @@ use futures::{
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "test-support")]
|
||||
use std::fmt;
|
||||
use std::{any::type_name, sync::Arc};
|
||||
use std::{any::type_name, fmt};
|
||||
pub use url::{Host, Url};
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -59,10 +59,10 @@ impl HttpRequestExt for http::request::Builder {
|
||||
}
|
||||
|
||||
pub trait HttpClient: 'static + Send + Sync {
|
||||
fn type_name(&self) -> &'static str;
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue>;
|
||||
|
||||
fn proxy(&self) -> Option<&Url>;
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<AsyncBody>,
|
||||
@@ -106,8 +106,6 @@ pub trait HttpClient: 'static + Send + Sync {
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
panic!("called as_fake on {}", type_name::<Self>())
|
||||
@@ -163,10 +161,6 @@ impl HttpClient for HttpClientWithProxy {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self.client.as_fake()
|
||||
@@ -182,19 +176,13 @@ impl HttpClient for HttpClientWithProxy {
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that has a base URL.
|
||||
#[derive(Deref)]
|
||||
pub struct HttpClientWithUrl {
|
||||
base_url: Mutex<String>,
|
||||
#[deref]
|
||||
client: HttpClientWithProxy,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for HttpClientWithUrl {
|
||||
type Target = HttpClientWithProxy;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClientWithUrl {
|
||||
/// Returns a new [`HttpClientWithUrl`] with the given base URL.
|
||||
pub fn new(
|
||||
@@ -314,10 +302,6 @@ impl HttpClient for HttpClientWithUrl {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self.client.as_fake()
|
||||
@@ -384,10 +368,6 @@ impl HttpClient for BlockedHttpClient {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
panic!("called as_fake on {}", type_name::<Self>())
|
||||
@@ -482,10 +462,6 @@ impl HttpClient for FakeHttpClient {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -217,6 +217,7 @@ pub enum IconName {
|
||||
SupermavenError,
|
||||
SupermavenInit,
|
||||
SwatchBook,
|
||||
SweepAi,
|
||||
Tab,
|
||||
Terminal,
|
||||
TerminalAlt,
|
||||
|
||||
@@ -184,7 +184,7 @@ enum SearchMode {
|
||||
impl SearchMode {
|
||||
fn invert(&self) -> Self {
|
||||
match self {
|
||||
SearchMode::Normal => SearchMode::KeyStroke { exact_match: false },
|
||||
SearchMode::Normal => SearchMode::KeyStroke { exact_match: true },
|
||||
SearchMode::KeyStroke { .. } => SearchMode::Normal,
|
||||
}
|
||||
}
|
||||
@@ -958,12 +958,14 @@ impl KeymapEditor {
|
||||
|
||||
let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
menu.context(self.focus_handle.clone())
|
||||
.when(selected_binding_is_unbound, |this| {
|
||||
this.action("Create", Box::new(CreateBinding))
|
||||
})
|
||||
.action_disabled_when(
|
||||
selected_binding_is_unbound,
|
||||
"Edit",
|
||||
Box::new(EditBinding),
|
||||
)
|
||||
.action("Create", Box::new(CreateBinding))
|
||||
.action_disabled_when(
|
||||
selected_binding_is_unbound,
|
||||
"Delete",
|
||||
@@ -1598,9 +1600,33 @@ impl Item for KeymapEditor {
|
||||
|
||||
impl Render for KeymapEditor {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
|
||||
if let SearchMode::KeyStroke { exact_match } = self.search_mode {
|
||||
let button = IconButton::new("keystrokes-exact-match", IconName::CaseSensitive)
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Toggle Exact Match Mode",
|
||||
&ToggleExactKeystrokeMatching,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.shape(IconButtonShape::Square)
|
||||
.toggle_state(exact_match)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(ToggleExactKeystrokeMatching.boxed_clone(), cx);
|
||||
}));
|
||||
|
||||
self.keystroke_editor.update(cx, |editor, _| {
|
||||
editor.actions_slot = Some(button.into_any_element());
|
||||
});
|
||||
} else {
|
||||
self.keystroke_editor.update(cx, |editor, _| {
|
||||
editor.actions_slot = None;
|
||||
});
|
||||
}
|
||||
|
||||
let row_count = self.matches.len();
|
||||
let theme = cx.theme();
|
||||
let focus_handle = &self.focus_handle;
|
||||
let theme = cx.theme();
|
||||
|
||||
v_flex()
|
||||
.id("keymap-editor")
|
||||
@@ -1784,49 +1810,14 @@ impl Render for KeymapEditor {
|
||||
)
|
||||
),
|
||||
)
|
||||
.when_some(
|
||||
match self.search_mode {
|
||||
SearchMode::Normal => None,
|
||||
SearchMode::KeyStroke { exact_match } => Some(exact_match),
|
||||
},
|
||||
|this, exact_match| {
|
||||
.when(
|
||||
matches!(self.search_mode, SearchMode::KeyStroke { .. }),
|
||||
|this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(self.keystroke_editor.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w_64()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"keystrokes-exact-match",
|
||||
IconName::CaseSensitive,
|
||||
)
|
||||
.tooltip({
|
||||
let keystroke_focus_handle =
|
||||
self.keystroke_editor.read(cx).focus_handle(cx);
|
||||
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Exact Match Mode",
|
||||
&ToggleExactKeystrokeMatching,
|
||||
&keystroke_focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.shape(IconButtonShape::Square)
|
||||
.toggle_state(exact_match)
|
||||
.on_click(
|
||||
cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
ToggleExactKeystrokeMatching.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(div().min_w_64()), // Spacer div to align with the search input
|
||||
)
|
||||
},
|
||||
),
|
||||
|
||||
@@ -64,6 +64,7 @@ pub struct KeystrokeInput {
|
||||
clear_close_keystrokes_timer: Option<Task<()>>,
|
||||
#[cfg(test)]
|
||||
recording: bool,
|
||||
pub actions_slot: Option<AnyElement>,
|
||||
}
|
||||
|
||||
impl KeystrokeInput {
|
||||
@@ -94,6 +95,7 @@ impl KeystrokeInput {
|
||||
clear_close_keystrokes_timer: None,
|
||||
#[cfg(test)]
|
||||
recording: false,
|
||||
actions_slot: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,6 +447,11 @@ impl KeystrokeInput {
|
||||
// not get de-synced
|
||||
self.inner_focus_handle.is_focused(window)
|
||||
}
|
||||
|
||||
pub fn actions_slot(mut self, action: impl IntoElement) -> Self {
|
||||
self.actions_slot = Some(action.into_any_element());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for KeystrokeInput {}
|
||||
@@ -586,7 +593,7 @@ impl Render for KeystrokeInput {
|
||||
.min_w_0()
|
||||
.justify_center()
|
||||
.flex_wrap()
|
||||
.gap(ui::DynamicSpacing::Base04.rems(cx))
|
||||
.gap_1()
|
||||
.children(self.render_keystrokes(is_recording)),
|
||||
)
|
||||
.child(
|
||||
@@ -636,18 +643,25 @@ impl Render for KeystrokeInput {
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
IconButton::new("clear-btn", IconName::Backspace)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Clear Keystrokes",
|
||||
&ClearKeystrokes,
|
||||
))
|
||||
.when(!is_focused, |this| this.icon_color(Color::Muted))
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.clear_keystrokes(&ClearKeystrokes, window, cx);
|
||||
})),
|
||||
),
|
||||
.when_some(self.actions_slot.take(), |this, action| this.child(action))
|
||||
.when(is_recording, |this| {
|
||||
this.child(
|
||||
IconButton::new("clear-btn", IconName::Backspace)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Clear Keystrokes",
|
||||
Some(&ClearKeystrokes),
|
||||
"Hit it three times to execute",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.when(!is_focused, |this| this.icon_color(Color::Muted))
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.clear_keystrokes(&ClearKeystrokes, window, cx);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ test-support = [
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-md",
|
||||
"settings/test-support",
|
||||
"util/test-support",
|
||||
]
|
||||
@@ -59,6 +60,7 @@ sum_tree.workspace = true
|
||||
task.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
tree-sitter-md = { workspace = true, optional = true }
|
||||
tree-sitter-python = { workspace = true, optional = true }
|
||||
tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
|
||||
@@ -823,6 +823,7 @@ pub struct BracketMatch {
|
||||
pub open_range: Range<usize>,
|
||||
pub close_range: Range<usize>,
|
||||
pub newline_only: bool,
|
||||
pub depth: usize,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
@@ -4136,6 +4137,7 @@ impl BufferSnapshot {
|
||||
while let Some(mat) = matches.peek() {
|
||||
let mut open = None;
|
||||
let mut close = None;
|
||||
let depth = mat.depth;
|
||||
let config = &configs[mat.grammar_index];
|
||||
let pattern = &config.patterns[mat.pattern_index];
|
||||
for capture in mat.captures {
|
||||
@@ -4161,6 +4163,7 @@ impl BufferSnapshot {
|
||||
open_range,
|
||||
close_range,
|
||||
newline_only: pattern.newline_only,
|
||||
depth,
|
||||
});
|
||||
}
|
||||
None
|
||||
@@ -4320,8 +4323,12 @@ impl BufferSnapshot {
|
||||
) -> impl Iterator<Item = BracketMatch> + '_ {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
|
||||
self.bracket_ranges(range.clone()).filter(move |pair| {
|
||||
pair.open_range.start <= range.start && pair.close_range.end >= range.end
|
||||
let result: Vec<_> = self.bracket_ranges(range.clone()).collect();
|
||||
let max_depth = result.iter().map(|mat| mat.depth).max().unwrap_or(0);
|
||||
result.into_iter().filter(move |pair| {
|
||||
pair.open_range.start <= range.start
|
||||
&& pair.close_range.end >= range.end
|
||||
&& pair.depth == max_depth
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2658,6 +2658,35 @@ pub fn rust_lang() -> Arc<Language> {
|
||||
Arc::new(language)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn markdown_lang() -> Arc<Language> {
|
||||
use std::borrow::Cow;
|
||||
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Markdown".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["md".into()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_md::LANGUAGE.into()),
|
||||
)
|
||||
.with_queries(LanguageQueries {
|
||||
brackets: Some(Cow::from(include_str!(
|
||||
"../../languages/src/markdown/brackets.scm"
|
||||
))),
|
||||
injections: Some(Cow::from(include_str!(
|
||||
"../../languages/src/markdown/injections.scm"
|
||||
))),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Could not parse markdown queries");
|
||||
Arc::new(language)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -138,7 +138,7 @@ pub enum LanguageModelCompletionError {
|
||||
provider: LanguageModelProviderName,
|
||||
message: String,
|
||||
},
|
||||
#[error("permission error with {provider}'s API: {message}")]
|
||||
#[error("Permission error with {provider}'s API: {message}")]
|
||||
PermissionError {
|
||||
provider: LanguageModelProviderName,
|
||||
message: String,
|
||||
@@ -176,6 +176,12 @@ pub enum LanguageModelCompletionError {
|
||||
error: serde_json::Error,
|
||||
},
|
||||
|
||||
#[error("The model refused to complete the request, possibly because of a security policy.")]
|
||||
Refusal,
|
||||
|
||||
#[error("The model could not respond because its maximum number of tokens has been exceeded.")]
|
||||
MaxTokens,
|
||||
|
||||
// TODO: Ideally this would be removed in favor of having a comprehensive list of errors.
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
@@ -515,6 +521,9 @@ pub struct LanguageModelToolUse {
|
||||
pub raw_input: String,
|
||||
pub input: serde_json::Value,
|
||||
pub is_input_complete: bool,
|
||||
/// Thought signature the model sent us. Some models require that this
|
||||
/// signature be preserved and sent back in conversation history for validation.
|
||||
pub thought_signature: Option<String>,
|
||||
}
|
||||
|
||||
pub struct LanguageModelTextStream {
|
||||
@@ -921,4 +930,85 @@ mod tests {
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_model_tool_use_serializes_with_signature() {
|
||||
use serde_json::json;
|
||||
|
||||
let tool_use = LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from("test_id"),
|
||||
name: "test_tool".into(),
|
||||
raw_input: json!({"arg": "value"}).to_string(),
|
||||
input: json!({"arg": "value"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: Some("test_signature".to_string()),
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(&tool_use).unwrap();
|
||||
|
||||
assert_eq!(serialized["id"], "test_id");
|
||||
assert_eq!(serialized["name"], "test_tool");
|
||||
assert_eq!(serialized["thought_signature"], "test_signature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_model_tool_use_deserializes_with_missing_signature() {
|
||||
use serde_json::json;
|
||||
|
||||
let json = json!({
|
||||
"id": "test_id",
|
||||
"name": "test_tool",
|
||||
"raw_input": "{\"arg\":\"value\"}",
|
||||
"input": {"arg": "value"},
|
||||
"is_input_complete": true
|
||||
});
|
||||
|
||||
let tool_use: LanguageModelToolUse = serde_json::from_value(json).unwrap();
|
||||
|
||||
assert_eq!(tool_use.id, LanguageModelToolUseId::from("test_id"));
|
||||
assert_eq!(tool_use.name.as_ref(), "test_tool");
|
||||
assert_eq!(tool_use.thought_signature, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_model_tool_use_round_trip_with_signature() {
|
||||
use serde_json::json;
|
||||
|
||||
let original = LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from("round_trip_id"),
|
||||
name: "round_trip_tool".into(),
|
||||
raw_input: json!({"key": "value"}).to_string(),
|
||||
input: json!({"key": "value"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: Some("round_trip_sig".to_string()),
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(&original).unwrap();
|
||||
let deserialized: LanguageModelToolUse = serde_json::from_value(serialized).unwrap();
|
||||
|
||||
assert_eq!(deserialized.id, original.id);
|
||||
assert_eq!(deserialized.name, original.name);
|
||||
assert_eq!(deserialized.thought_signature, original.thought_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_model_tool_use_round_trip_without_signature() {
|
||||
use serde_json::json;
|
||||
|
||||
let original = LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from("no_sig_id"),
|
||||
name: "no_sig_tool".into(),
|
||||
raw_input: json!({"key": "value"}).to_string(),
|
||||
input: json!({"key": "value"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(&original).unwrap();
|
||||
let deserialized: LanguageModelToolUse = serde_json::from_value(serialized).unwrap();
|
||||
|
||||
assert_eq!(deserialized.id, original.id);
|
||||
assert_eq!(deserialized.name, original.name);
|
||||
assert_eq!(deserialized.thought_signature, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,6 +711,7 @@ impl AnthropicEventMapper {
|
||||
is_input_complete: false,
|
||||
raw_input: tool_use.input_json.clone(),
|
||||
input,
|
||||
thought_signature: None,
|
||||
},
|
||||
))];
|
||||
}
|
||||
@@ -734,6 +735,7 @@ impl AnthropicEventMapper {
|
||||
is_input_complete: true,
|
||||
input,
|
||||
raw_input: tool_use.input_json.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
)),
|
||||
Err(json_parse_err) => {
|
||||
|
||||
@@ -24,7 +24,10 @@ use bedrock::{
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use credentials_provider::CredentialsProvider;
|
||||
use futures::{FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyView, App, AsyncApp, Context, Entity, FontWeight, Subscription, Task};
|
||||
use gpui::{
|
||||
AnyView, App, AsyncApp, Context, Entity, FocusHandle, FontWeight, Subscription, Task, Window,
|
||||
actions,
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
use http_client::HttpClient;
|
||||
use language_model::{
|
||||
@@ -47,6 +50,8 @@ use util::ResultExt;
|
||||
|
||||
use crate::AllLanguageModelSettings;
|
||||
|
||||
actions!(bedrock, [Tab, TabPrev]);
|
||||
|
||||
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("amazon-bedrock");
|
||||
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Amazon Bedrock");
|
||||
|
||||
@@ -965,6 +970,7 @@ pub fn map_to_language_model_completion_events(
|
||||
is_input_complete: true,
|
||||
raw_input: tool_use.input_json,
|
||||
input,
|
||||
thought_signature: None,
|
||||
},
|
||||
))
|
||||
}),
|
||||
@@ -1012,6 +1018,7 @@ struct ConfigurationView {
|
||||
region_editor: Entity<InputField>,
|
||||
state: Entity<State>,
|
||||
load_credentials_task: Option<Task<()>>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
@@ -1022,11 +1029,41 @@ impl ConfigurationView {
|
||||
const PLACEHOLDER_REGION: &'static str = "us-east-1";
|
||||
|
||||
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
cx.observe(&state, |_, _, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
let access_key_id_editor = cx.new(|cx| {
|
||||
InputField::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT)
|
||||
.label("Access Key ID")
|
||||
.tab_index(0)
|
||||
.tab_stop(true)
|
||||
});
|
||||
|
||||
let secret_access_key_editor = cx.new(|cx| {
|
||||
InputField::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT)
|
||||
.label("Secret Access Key")
|
||||
.tab_index(1)
|
||||
.tab_stop(true)
|
||||
});
|
||||
|
||||
let session_token_editor = cx.new(|cx| {
|
||||
InputField::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT)
|
||||
.label("Session Token (Optional)")
|
||||
.tab_index(2)
|
||||
.tab_stop(true)
|
||||
});
|
||||
|
||||
let region_editor = cx.new(|cx| {
|
||||
InputField::new(window, cx, Self::PLACEHOLDER_REGION)
|
||||
.label("Region")
|
||||
.tab_index(3)
|
||||
.tab_stop(true)
|
||||
});
|
||||
|
||||
let load_credentials_task = Some(cx.spawn({
|
||||
let state = state.clone();
|
||||
async move |this, cx| {
|
||||
@@ -1046,22 +1083,13 @@ impl ConfigurationView {
|
||||
}));
|
||||
|
||||
Self {
|
||||
access_key_id_editor: cx.new(|cx| {
|
||||
InputField::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT)
|
||||
.label("Access Key ID")
|
||||
}),
|
||||
secret_access_key_editor: cx.new(|cx| {
|
||||
InputField::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT)
|
||||
.label("Secret Access Key")
|
||||
}),
|
||||
session_token_editor: cx.new(|cx| {
|
||||
InputField::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT)
|
||||
.label("Session Token (Optional)")
|
||||
}),
|
||||
region_editor: cx
|
||||
.new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_REGION).label("Region")),
|
||||
access_key_id_editor,
|
||||
secret_access_key_editor,
|
||||
session_token_editor,
|
||||
region_editor,
|
||||
state,
|
||||
load_credentials_task,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1141,6 +1169,19 @@ impl ConfigurationView {
|
||||
fn should_render_editor(&self, cx: &Context<Self>) -> bool {
|
||||
self.state.read(cx).is_authenticated()
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_next();
|
||||
}
|
||||
|
||||
fn on_tab_prev(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
window.focus_prev();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
@@ -1190,6 +1231,9 @@ impl Render for ConfigurationView {
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::on_tab))
|
||||
.on_action(cx.listener(Self::on_tab_prev))
|
||||
.on_action(cx.listener(ConfigurationView::save_credentials))
|
||||
.child(Label::new("To use Zed's agent with Bedrock, you can set a custom authentication strategy through the settings.json, or use static credentials."))
|
||||
.child(Label::new("But, to access models on AWS, you need to:").mt_1())
|
||||
@@ -1234,6 +1278,7 @@ impl ConfigurationView {
|
||||
fn render_static_credentials_ui(&self) -> impl IntoElement {
|
||||
v_flex()
|
||||
.my_2()
|
||||
.tab_group()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new("Static Keys")
|
||||
|
||||
@@ -143,9 +143,11 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
|
||||
};
|
||||
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return Task::ready( Err(anyhow!(
|
||||
"Copilot must be enabled for Copilot Chat to work. Please enable Copilot and try again."
|
||||
).into()));
|
||||
return Task::ready(Err(anyhow!(concat!(
|
||||
"Copilot must be enabled for Copilot Chat to work. ",
|
||||
"Please enable Copilot and try again."
|
||||
))
|
||||
.into()));
|
||||
};
|
||||
|
||||
let err = match copilot.read(cx).status() {
|
||||
@@ -456,6 +458,7 @@ pub fn map_to_language_model_completion_events(
|
||||
is_input_complete: true,
|
||||
input,
|
||||
raw_input: tool_call.arguments,
|
||||
thought_signature: None,
|
||||
},
|
||||
)),
|
||||
Err(error) => Ok(
|
||||
@@ -558,6 +561,7 @@ impl CopilotResponsesEventMapper {
|
||||
is_input_complete: true,
|
||||
input,
|
||||
raw_input: arguments.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
))),
|
||||
Err(error) => {
|
||||
|
||||
@@ -501,6 +501,7 @@ impl DeepSeekEventMapper {
|
||||
is_input_complete: true,
|
||||
input,
|
||||
raw_input: tool_call.arguments.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
)),
|
||||
Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
|
||||
@@ -439,11 +439,15 @@ pub fn into_google(
|
||||
})]
|
||||
}
|
||||
language_model::MessageContent::ToolUse(tool_use) => {
|
||||
// Normalize empty string signatures to None
|
||||
let thought_signature = tool_use.thought_signature.filter(|s| !s.is_empty());
|
||||
|
||||
vec![Part::FunctionCallPart(google_ai::FunctionCallPart {
|
||||
function_call: google_ai::FunctionCall {
|
||||
name: tool_use.name.to_string(),
|
||||
args: tool_use.input,
|
||||
},
|
||||
thought_signature,
|
||||
})]
|
||||
}
|
||||
language_model::MessageContent::ToolResult(tool_result) => {
|
||||
@@ -655,6 +659,11 @@ impl GoogleEventMapper {
|
||||
let id: LanguageModelToolUseId =
|
||||
format!("{}-{}", name, next_tool_id).into();
|
||||
|
||||
// Normalize empty string signatures to None
|
||||
let thought_signature = function_call_part
|
||||
.thought_signature
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
events.push(Ok(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id,
|
||||
@@ -662,6 +671,7 @@ impl GoogleEventMapper {
|
||||
is_input_complete: true,
|
||||
raw_input: function_call_part.function_call.args.to_string(),
|
||||
input: function_call_part.function_call.args,
|
||||
thought_signature,
|
||||
},
|
||||
)));
|
||||
}
|
||||
@@ -891,3 +901,424 @@ impl Render for ConfigurationView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use google_ai::{
|
||||
Content, FunctionCall, FunctionCallPart, GenerateContentCandidate, GenerateContentResponse,
|
||||
Part, Role as GoogleRole, TextPart,
|
||||
};
|
||||
use language_model::{LanguageModelToolUseId, MessageContent, Role};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_function_call_with_signature_creates_tool_use_with_signature() {
|
||||
let mut mapper = GoogleEventMapper::new();
|
||||
|
||||
let response = GenerateContentResponse {
|
||||
candidates: Some(vec![GenerateContentCandidate {
|
||||
index: Some(0),
|
||||
content: Content {
|
||||
parts: vec![Part::FunctionCallPart(FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "test_function".to_string(),
|
||||
args: json!({"arg": "value"}),
|
||||
},
|
||||
thought_signature: Some("test_signature_123".to_string()),
|
||||
})],
|
||||
role: GoogleRole::Model,
|
||||
},
|
||||
finish_reason: None,
|
||||
finish_message: None,
|
||||
safety_ratings: None,
|
||||
citation_metadata: None,
|
||||
}]),
|
||||
prompt_feedback: None,
|
||||
usage_metadata: None,
|
||||
};
|
||||
|
||||
let events = mapper.map_event(response);
|
||||
|
||||
assert_eq!(events.len(), 2); // ToolUse event + Stop event
|
||||
|
||||
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
|
||||
assert_eq!(tool_use.name.as_ref(), "test_function");
|
||||
assert_eq!(
|
||||
tool_use.thought_signature.as_deref(),
|
||||
Some("test_signature_123")
|
||||
);
|
||||
} else {
|
||||
panic!("Expected ToolUse event");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_call_without_signature_has_none() {
|
||||
let mut mapper = GoogleEventMapper::new();
|
||||
|
||||
let response = GenerateContentResponse {
|
||||
candidates: Some(vec![GenerateContentCandidate {
|
||||
index: Some(0),
|
||||
content: Content {
|
||||
parts: vec![Part::FunctionCallPart(FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "test_function".to_string(),
|
||||
args: json!({"arg": "value"}),
|
||||
},
|
||||
thought_signature: None,
|
||||
})],
|
||||
role: GoogleRole::Model,
|
||||
},
|
||||
finish_reason: None,
|
||||
finish_message: None,
|
||||
safety_ratings: None,
|
||||
citation_metadata: None,
|
||||
}]),
|
||||
prompt_feedback: None,
|
||||
usage_metadata: None,
|
||||
};
|
||||
|
||||
let events = mapper.map_event(response);
|
||||
|
||||
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
|
||||
assert_eq!(tool_use.thought_signature, None);
|
||||
} else {
|
||||
panic!("Expected ToolUse event");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_string_signature_normalized_to_none() {
|
||||
let mut mapper = GoogleEventMapper::new();
|
||||
|
||||
let response = GenerateContentResponse {
|
||||
candidates: Some(vec![GenerateContentCandidate {
|
||||
index: Some(0),
|
||||
content: Content {
|
||||
parts: vec![Part::FunctionCallPart(FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "test_function".to_string(),
|
||||
args: json!({"arg": "value"}),
|
||||
},
|
||||
thought_signature: Some("".to_string()),
|
||||
})],
|
||||
role: GoogleRole::Model,
|
||||
},
|
||||
finish_reason: None,
|
||||
finish_message: None,
|
||||
safety_ratings: None,
|
||||
citation_metadata: None,
|
||||
}]),
|
||||
prompt_feedback: None,
|
||||
usage_metadata: None,
|
||||
};
|
||||
|
||||
let events = mapper.map_event(response);
|
||||
|
||||
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
|
||||
assert_eq!(tool_use.thought_signature, None);
|
||||
} else {
|
||||
panic!("Expected ToolUse event");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parallel_function_calls_preserve_signatures() {
|
||||
let mut mapper = GoogleEventMapper::new();
|
||||
|
||||
let response = GenerateContentResponse {
|
||||
candidates: Some(vec![GenerateContentCandidate {
|
||||
index: Some(0),
|
||||
content: Content {
|
||||
parts: vec![
|
||||
Part::FunctionCallPart(FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "function_1".to_string(),
|
||||
args: json!({"arg": "value1"}),
|
||||
},
|
||||
thought_signature: Some("signature_1".to_string()),
|
||||
}),
|
||||
Part::FunctionCallPart(FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "function_2".to_string(),
|
||||
args: json!({"arg": "value2"}),
|
||||
},
|
||||
thought_signature: None,
|
||||
}),
|
||||
],
|
||||
role: GoogleRole::Model,
|
||||
},
|
||||
finish_reason: None,
|
||||
finish_message: None,
|
||||
safety_ratings: None,
|
||||
citation_metadata: None,
|
||||
}]),
|
||||
prompt_feedback: None,
|
||||
usage_metadata: None,
|
||||
};
|
||||
|
||||
let events = mapper.map_event(response);
|
||||
|
||||
assert_eq!(events.len(), 3); // 2 ToolUse events + Stop event
|
||||
|
||||
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
|
||||
assert_eq!(tool_use.name.as_ref(), "function_1");
|
||||
assert_eq!(tool_use.thought_signature.as_deref(), Some("signature_1"));
|
||||
} else {
|
||||
panic!("Expected ToolUse event for function_1");
|
||||
}
|
||||
|
||||
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[1] {
|
||||
assert_eq!(tool_use.name.as_ref(), "function_2");
|
||||
assert_eq!(tool_use.thought_signature, None);
|
||||
} else {
|
||||
panic!("Expected ToolUse event for function_2");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_use_with_signature_converts_to_function_call_part() {
|
||||
let tool_use = language_model::LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from("test_id"),
|
||||
name: "test_function".into(),
|
||||
raw_input: json!({"arg": "value"}).to_string(),
|
||||
input: json!({"arg": "value"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: Some("test_signature_456".to_string()),
|
||||
};
|
||||
|
||||
let request = super::into_google(
|
||||
LanguageModelRequest {
|
||||
messages: vec![language_model::LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![MessageContent::ToolUse(tool_use)],
|
||||
cache: false,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
"gemini-2.5-flash".to_string(),
|
||||
GoogleModelMode::Default,
|
||||
);
|
||||
|
||||
assert_eq!(request.contents[0].parts.len(), 1);
|
||||
if let Part::FunctionCallPart(fc_part) = &request.contents[0].parts[0] {
|
||||
assert_eq!(fc_part.function_call.name, "test_function");
|
||||
assert_eq!(
|
||||
fc_part.thought_signature.as_deref(),
|
||||
Some("test_signature_456")
|
||||
);
|
||||
} else {
|
||||
panic!("Expected FunctionCallPart");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_use_without_signature_omits_field() {
|
||||
let tool_use = language_model::LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from("test_id"),
|
||||
name: "test_function".into(),
|
||||
raw_input: json!({"arg": "value"}).to_string(),
|
||||
input: json!({"arg": "value"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let request = super::into_google(
|
||||
LanguageModelRequest {
|
||||
messages: vec![language_model::LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![MessageContent::ToolUse(tool_use)],
|
||||
cache: false,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
"gemini-2.5-flash".to_string(),
|
||||
GoogleModelMode::Default,
|
||||
);
|
||||
|
||||
assert_eq!(request.contents[0].parts.len(), 1);
|
||||
if let Part::FunctionCallPart(fc_part) = &request.contents[0].parts[0] {
|
||||
assert_eq!(fc_part.thought_signature, None);
|
||||
} else {
|
||||
panic!("Expected FunctionCallPart");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_signature_in_tool_use_normalized_to_none() {
|
||||
let tool_use = language_model::LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from("test_id"),
|
||||
name: "test_function".into(),
|
||||
raw_input: json!({"arg": "value"}).to_string(),
|
||||
input: json!({"arg": "value"}),
|
||||
is_input_complete: true,
|
||||
thought_signature: Some("".to_string()),
|
||||
};
|
||||
|
||||
let request = super::into_google(
|
||||
LanguageModelRequest {
|
||||
messages: vec![language_model::LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![MessageContent::ToolUse(tool_use)],
|
||||
cache: false,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
"gemini-2.5-flash".to_string(),
|
||||
GoogleModelMode::Default,
|
||||
);
|
||||
|
||||
if let Part::FunctionCallPart(fc_part) = &request.contents[0].parts[0] {
|
||||
assert_eq!(fc_part.thought_signature, None);
|
||||
} else {
|
||||
panic!("Expected FunctionCallPart");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_trip_preserves_signature() {
|
||||
let mut mapper = GoogleEventMapper::new();
|
||||
|
||||
// Simulate receiving a response from Google with a signature
|
||||
let response = GenerateContentResponse {
|
||||
candidates: Some(vec![GenerateContentCandidate {
|
||||
index: Some(0),
|
||||
content: Content {
|
||||
parts: vec![Part::FunctionCallPart(FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "test_function".to_string(),
|
||||
args: json!({"arg": "value"}),
|
||||
},
|
||||
thought_signature: Some("round_trip_sig".to_string()),
|
||||
})],
|
||||
role: GoogleRole::Model,
|
||||
},
|
||||
finish_reason: None,
|
||||
finish_message: None,
|
||||
safety_ratings: None,
|
||||
citation_metadata: None,
|
||||
}]),
|
||||
prompt_feedback: None,
|
||||
usage_metadata: None,
|
||||
};
|
||||
|
||||
let events = mapper.map_event(response);
|
||||
|
||||
let tool_use = if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
|
||||
tool_use.clone()
|
||||
} else {
|
||||
panic!("Expected ToolUse event");
|
||||
};
|
||||
|
||||
// Convert back to Google format
|
||||
let request = super::into_google(
|
||||
LanguageModelRequest {
|
||||
messages: vec![language_model::LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![MessageContent::ToolUse(tool_use)],
|
||||
cache: false,
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
"gemini-2.5-flash".to_string(),
|
||||
GoogleModelMode::Default,
|
||||
);
|
||||
|
||||
// Verify signature is preserved
|
||||
if let Part::FunctionCallPart(fc_part) = &request.contents[0].parts[0] {
|
||||
assert_eq!(fc_part.thought_signature.as_deref(), Some("round_trip_sig"));
|
||||
} else {
|
||||
panic!("Expected FunctionCallPart");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_text_and_function_call_with_signature() {
|
||||
let mut mapper = GoogleEventMapper::new();
|
||||
|
||||
let response = GenerateContentResponse {
|
||||
candidates: Some(vec![GenerateContentCandidate {
|
||||
index: Some(0),
|
||||
content: Content {
|
||||
parts: vec![
|
||||
Part::TextPart(TextPart {
|
||||
text: "I'll help with that.".to_string(),
|
||||
}),
|
||||
Part::FunctionCallPart(FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "helper_function".to_string(),
|
||||
args: json!({"query": "help"}),
|
||||
},
|
||||
thought_signature: Some("mixed_sig".to_string()),
|
||||
}),
|
||||
],
|
||||
role: GoogleRole::Model,
|
||||
},
|
||||
finish_reason: None,
|
||||
finish_message: None,
|
||||
safety_ratings: None,
|
||||
citation_metadata: None,
|
||||
}]),
|
||||
prompt_feedback: None,
|
||||
usage_metadata: None,
|
||||
};
|
||||
|
||||
let events = mapper.map_event(response);
|
||||
|
||||
assert_eq!(events.len(), 3); // Text event + ToolUse event + Stop event
|
||||
|
||||
if let Ok(LanguageModelCompletionEvent::Text(text)) = &events[0] {
|
||||
assert_eq!(text, "I'll help with that.");
|
||||
} else {
|
||||
panic!("Expected Text event");
|
||||
}
|
||||
|
||||
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[1] {
|
||||
assert_eq!(tool_use.name.as_ref(), "helper_function");
|
||||
assert_eq!(tool_use.thought_signature.as_deref(), Some("mixed_sig"));
|
||||
} else {
|
||||
panic!("Expected ToolUse event");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_special_characters_in_signature_preserved() {
|
||||
let mut mapper = GoogleEventMapper::new();
|
||||
|
||||
let signature_with_special_chars = "sig<>\"'&%$#@!{}[]".to_string();
|
||||
|
||||
let response = GenerateContentResponse {
|
||||
candidates: Some(vec![GenerateContentCandidate {
|
||||
index: Some(0),
|
||||
content: Content {
|
||||
parts: vec![Part::FunctionCallPart(FunctionCallPart {
|
||||
function_call: FunctionCall {
|
||||
name: "test_function".to_string(),
|
||||
args: json!({"arg": "value"}),
|
||||
},
|
||||
thought_signature: Some(signature_with_special_chars.clone()),
|
||||
})],
|
||||
role: GoogleRole::Model,
|
||||
},
|
||||
finish_reason: None,
|
||||
finish_message: None,
|
||||
safety_ratings: None,
|
||||
citation_metadata: None,
|
||||
}]),
|
||||
prompt_feedback: None,
|
||||
usage_metadata: None,
|
||||
};
|
||||
|
||||
let events = mapper.map_event(response);
|
||||
|
||||
if let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] {
|
||||
assert_eq!(
|
||||
tool_use.thought_signature.as_deref(),
|
||||
Some(signature_with_special_chars.as_str())
|
||||
);
|
||||
} else {
|
||||
panic!("Expected ToolUse event");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,6 +569,7 @@ impl LmStudioEventMapper {
|
||||
is_input_complete: true,
|
||||
input,
|
||||
raw_input: tool_call.arguments,
|
||||
thought_signature: None,
|
||||
},
|
||||
)),
|
||||
Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
|
||||
@@ -720,6 +720,7 @@ impl MistralEventMapper {
|
||||
is_input_complete: true,
|
||||
input,
|
||||
raw_input: tool_call.arguments,
|
||||
thought_signature: None,
|
||||
},
|
||||
))),
|
||||
Err(error) => {
|
||||
|
||||
@@ -592,6 +592,7 @@ fn map_to_language_model_completion_events(
|
||||
raw_input: function.arguments.to_string(),
|
||||
input: function.arguments,
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
});
|
||||
events.push(Ok(event));
|
||||
state.used_tools = true;
|
||||
|
||||
@@ -586,6 +586,7 @@ impl OpenAiEventMapper {
|
||||
is_input_complete: true,
|
||||
input,
|
||||
raw_input: tool_call.arguments.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
)),
|
||||
Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user